Skip to content

Database Selection Guide: From Classical to Edge - A Complete Engineering Perspective

Comprehensive guide to choosing the right database for your project - covering SQL, NoSQL, NewSQL, and edge solutions with real-world implementation stories and performance benchmarks.

Database selection mistakes can be costly. A simple product catalog that works perfectly with 1,000 products can suddenly ground to a halt at 100,000. When MongoDB collections aren't indexed properly and queries scan entire collections, that "web-scale" solution becomes an expensive lesson in database fundamentals.

This isn't uncommon. Database choices derail more projects than most engineers realize. The database is your application's foundation - get it wrong, and everything built on top starts to crack.

Why Database Choice Matters More Than You Think

The Real Cost of Wrong Choices

Technical Debt Explosion: Switching databases mid-project isn't just a migration - it's architectural surgery. Using MySQL for time-series data creates query complexity with excessive date functions and subqueries. Moving to a specialized database like InfluxDB can take 6 months and require rewriting significant application logic.

Team Productivity Impact: Your choice directly affects development velocity. A team comfortable with SQL struggling with MongoDB document queries can lose 40% productivity for months. Conversely, NoSQL experts forced into rigid SQL schemas often over-engineer simple problems.

Hidden Operational Costs:

  • PostgreSQL self-hosted: $200/month server + 20 hours admin time
  • RDS PostgreSQL: $400/month but 2 hours admin time
  • DynamoDB: $50/month usage + zero admin time (when designed properly)

The cheapest option often costs the most when you factor in engineering time.

Classical Database Categories

Relational (SQL) Databases

PostgreSQL: The Swiss Army Knife

PostgreSQL is an excellent default choice for most projects. It's boring in the best possible way - reliable, well-documented, and handles edge cases gracefully. PostgreSQL 16 (current major version) continues to improve performance and adds features like SQL/JSON standard support.

When PostgreSQL Shines:

  • Complex business logic requiring ACID transactions
  • Analytics workloads with sophisticated queries
  • Applications needing both relational and document storage (JSONB)
  • Teams comfortable with SQL

Production Story: We migrated a Rails app from MySQL to PostgreSQL specifically for its JSON capabilities. Being able to store flexible metadata alongside relational data eliminated the need for a separate document store. Query performance improved 3x, and we avoided the complexity of maintaining two database systems.

typescript
// PostgreSQL with JSONB - best of both worldsconst user = await db.query(`  SELECT id, email,          preferences->>'theme' as theme,         preferences->'notifications'->>'email' as email_notifications  FROM users   WHERE preferences @> '{"beta_features": true}'`);

Gotchas:

  • Write amplification with frequent updates (use HOT updates wisely)
  • Connection management - use pgBouncer in production
  • Vacuum tuning required for high-write workloads

MySQL: The Web-Scale Workhorse

MySQL earned its reputation powering the web's biggest sites. It's fast, well-understood, and has an ecosystem built around web applications.

When MySQL Works:

  • Read-heavy web applications
  • Applications requiring master-slave replication
  • Teams with existing MySQL expertise
  • Cost-conscious projects (excellent community support)

Real-World Performance: MySQL clusters can handle 50K+ QPS across multiple read replicas. The key is treating it like a cache layer - denormalized data, aggressive indexing, and strategic partitioning.

sql
-- MySQL optimized for read performanceCREATE TABLE user_stats (  user_id INT PRIMARY KEY,  total_orders INT DEFAULT 0,  last_order_date DATE,  lifetime_value DECIMAL(10,2),  INDEX idx_lifetime_value (lifetime_value DESC),  INDEX idx_last_order (last_order_date)) ENGINE=InnoDB;

Trade-offs:

  • Less sophisticated query planner than PostgreSQL
  • JSON support exists but feels bolted-on
  • Replication lag can be tricky in multi-master setups

SQLite: The Embedded Champion

Don't underestimate SQLite. It's not just for mobile apps anymore. With proper configuration, it can handle surprising workloads.

Perfect For:

  • Edge applications with local data requirements
  • Development and testing environments
  • Applications with <100GB data and modest concurrency
  • Embedded systems and IoT devices

Performance Reality Check: SQLite can handle 100K reads/second on modern hardware. The bottleneck is usually concurrent writes, not reads.

typescript
// SQLite with WAL mode for better concurrencyconst db = new Database('app.db', {  pragma: {    journal_mode: 'WAL',    synchronous: 'NORMAL',    cache_size: -64000, // 64MB cache    temp_store: 'MEMORY'  }});

NoSQL Databases

MongoDB: The Document Store

MongoDB gets a lot of hate, often deserved, but it genuinely excels in specific scenarios. The key is understanding its strengths and designing around its limitations.

Where MongoDB Excels:

  • Rapid prototyping with evolving schemas
  • Content management systems
  • Catalog systems with varied product attributes
  • Applications where document structure matches business logic

Important Consideration: Always design your indexes first. MongoDB without proper indexes is like a Ferrari without wheels - impressive specs but unusable performance.

javascript
// MongoDB indexing strategy for e-commercedb.products.createIndex({  "category": 1,  "price": 1,  "createdAt": -1});
// Compound index for faceted searchdb.products.createIndex({  "category": 1,  "attributes.brand": 1,  "attributes.color": 1,  "price": 1});

Production Gotchas:

  • Memory usage grows with working set size
  • Aggregation pipelines can be memory-intensive
  • Sharding requires careful planning of shard keys

Redis: The Speed Demon

Redis isn't just a cache - it's a data structure server that can solve complex problems elegantly.

Redis Use Cases Beyond Caching:

  • Session storage with automatic expiration
  • Rate limiting with sliding windows
  • Real-time leaderboards and counters
  • Pub/sub for real-time features
  • Distributed locks for coordination

Proven Pattern: Using Redis for distributed rate limiting across microservices:

typescript
// Sliding window rate limiter in Redisasync function checkRateLimit(userId: string, limit: number, windowMs: number) {  const key = `rate_limit:${userId}`;  const now = Date.now();  const windowStart = now - windowMs;    const pipeline = redis.pipeline();  pipeline.zremrangebyscore(key, 0, windowStart);  pipeline.zadd(key, now, now);  pipeline.zcard(key);  pipeline.expire(key, Math.ceil(windowMs / 1000));    const results = await pipeline.exec();  const currentCount = results[2][1] as number;    return currentCount <= limit;}

DynamoDB: The Serverless Powerhouse

DynamoDB is either amazing or terrible depending on how well you understand its data model. There's no middle ground.

DynamoDB Strengths:

  • True serverless with pay-per-use pricing
  • Predictable single-digit millisecond latency
  • Automatic scaling and backup
  • Global tables for multi-region applications

The DynamoDB Mental Model: Stop thinking in SQL. Start thinking in access patterns. Design your table structure around how you'll query the data, not how you'll store it.

typescript
// DynamoDB single-table design patterninterface GameRecord {  PK: string;  // USER#123 or GAME#456  SK: string;  // PROFILE or SCORE#2024-01-15  Type: string;  // USER or GAME or SCORE  GSI1PK?: string; // For secondary access patterns  GSI1SK?: string;  // ... other attributes}
// Query user's recent scoresconst scores = await dynamodb.query({  TableName: 'GameData',  KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',  ExpressionAttributeValues: {    ':pk': 'USER#123',    ':sk': 'SCORE#'  },  ScanIndexForward: false, // Latest first  Limit: 10}).promise();

DynamoDB Gotchas:

  • Hot partitions can throttle your entire application
  • Query patterns must be known upfront
  • Complex relationships require careful GSI design
  • FilterExpressions still consume read capacity

NewSQL: Best of Both Worlds

CockroachDB: Distributed SQL Done Right

CockroachDB promises PostgreSQL compatibility with global distribution. In practice, it delivers on most of these promises with some important caveats. CockroachDB v23.2 (current stable) offers improved PostgreSQL compatibility and better performance for distributed workloads.

When CockroachDB Makes Sense:

  • Global applications requiring strong consistency
  • Financial systems needing ACID across regions
  • Applications outgrowing single-node PostgreSQL
  • Teams wanting SQL with automatic sharding

Implementation Example: CockroachDB works well for fintech applications spanning multiple regions. The automatic geo-partitioning keeps user data in the right regions for compliance, while maintaining strong consistency for financial transactions.

sql
-- CockroachDB geo-partitioningCREATE TABLE users (  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),  email STRING UNIQUE,  region STRING NOT NULL,  created_at TIMESTAMPTZ DEFAULT now()) PARTITION BY LIST (region);
CREATE PARTITION us_users VALUES IN ('us-east', 'us-west');CREATE PARTITION eu_users VALUES IN ('eu-west', 'eu-central');

Trade-offs:

  • Higher latency than single-node databases due to consensus
  • More expensive than traditional PostgreSQL
  • Some PostgreSQL features still missing or different

Edge Database Solutions

PouchDB/CouchDB: Offline-First Architecture

For applications that need to work offline, CouchDB's replication model is unmatched. PouchDB brings this to the browser seamlessly.

Perfect For:

  • Field service applications
  • Mobile apps in areas with poor connectivity
  • Collaborative applications with eventual consistency needs

Implementation Pattern:

javascript
// PouchDB offline-first patternconst localDB = new PouchDB('local-data');const remoteDB = new PouchDB('https://server.com/data');
// Two-way sync with conflict resolutionconst sync = localDB.sync(remoteDB, {  live: true,  retry: true}).on('change', (info) => {  console.log('Sync change:', info);}).on('error', (err) => {  console.log('Sync error:', err);});
// App works offline, syncs when onlineawait localDB.put({  _id: 'user-123',  name: 'John Doe',  lastModified: new Date().toISOString()});

InfluxDB: Time-Series Specialist

When you're dealing with metrics, logs, or IoT data, specialized time-series databases like InfluxDB outperform general-purpose databases dramatically.

InfluxDB Advantages:

  • Automatic downsampling and retention policies
  • Built-in time-based functions and aggregations
  • Efficient storage for time-series data
  • Native integration with monitoring tools
influxql
-- InfluxDB query for system metricsSELECT mean("cpu_usage") FROM "system_metrics" WHERE time >= now() - 24h GROUP BY time(1h), "host"

Database Selection Matrix

By Use Case

E-commerce Platform:

  • Catalog: PostgreSQL (structured product data + JSONB for attributes)
  • Sessions: Redis (fast access + automatic expiration)
  • Orders: PostgreSQL (ACID compliance for financial data)
  • Analytics: ClickHouse or BigQuery (analytical workloads)

IoT Application:

  • Device State: Redis (real-time updates)
  • Time Series: InfluxDB (sensor data)
  • Configuration: PostgreSQL (device management)
  • Edge Cache: SQLite (local device storage)

Social Media App:

  • User Profiles: PostgreSQL (relational data)
  • Posts/Timeline: DynamoDB (high scale, simple queries)
  • Real-time: Redis Streams (notifications, chat)
  • Search: Elasticsearch (content discovery)

By Scale Requirements

Small Scale (1K-100K users): PostgreSQL + Redis covers 90% of use cases. Simple, well-understood, cost-effective.

Medium Scale (100K-10M users):

  • Read replicas for PostgreSQL
  • DynamoDB for high-traffic features
  • Elasticsearch for search
  • Redis cluster for caching

Large Scale (10M+ users):

  • Sharded PostgreSQL or CockroachDB
  • DynamoDB with careful partition design
  • Redis Cluster with consistent hashing
  • Specialized databases for specific workloads

Selection Criteria Deep Dive

Consistency Requirements

Strong Consistency (ACID): PostgreSQL, CockroachDB, SQL Server

  • Financial transactions
  • Inventory management
  • User authentication

Eventual Consistency (BASE): DynamoDB, MongoDB, Cassandra

  • Social media feeds
  • Content catalogs
  • Analytics data

Choose Strong When: Data integrity is more important than availability Choose Eventual When: Availability and partition tolerance are priority

Performance Patterns

Read-Heavy Workloads: MySQL with read replicas, Redis caching layer Write-Heavy Workloads: DynamoDB, Cassandra, or sharded PostgreSQL Mixed Workloads: PostgreSQL with proper indexing and connection pooling

Latency Requirements:

  • <1ms: Redis (in-memory)
  • <10ms: DynamoDB, well-tuned PostgreSQL
  • <100ms: Most SQL databases with proper indexing
  • 100ms: Acceptable for analytical workloads

Real-World Migration Stories

The MongoDB to PostgreSQL Migration

The Problem: Content management systems using MongoDB can struggle with complex queries. Aggregation pipelines become unmaintainable, and the lack of schema validation causes data quality issues.

The Solution: Migrated to PostgreSQL with JSONB columns for flexible content, maintaining the benefits of document storage while gaining SQL's query power.

Timeline: 3 months with zero downtime using a dual-write pattern:

typescript
// Dual-write migration patternclass ContentService {  async createPost(post: Post) {    // Write to new PostgreSQL database    const pgResult = await this.postgresDB.insert(post);        try {      // Write to legacy MongoDB (for rollback safety)      await this.mongoDB.insertOne(post);    } catch (error) {      // MongoDB failure shouldn't break the flow      console.error('MongoDB write failed:', error);    }        return pgResult;  }}

Outcomes:

  • Schema validation in PostgreSQL catches data quality issues early
  • JSONB queries can be faster than MongoDB aggregations for complex queries
  • Proper database selection significantly improves developer productivity

The Single-Region to Multi-Region Challenge

The Challenge: Growing SaaS applications need to expand from single-region to global, requiring data residency compliance and low latency worldwide.

The Solution: Migrating from single PostgreSQL to CockroachDB with geo-partitioning allows user data to stay in their regions while maintaining global consistency for billing and analytics.

Implementation:

sql
-- Geo-partitioned user dataALTER TABLE users CONFIGURE ZONE USING constraints = '[+region=us-east1]';ALTER TABLE user_profiles CONFIGURE ZONE USING constraints = '[+region=us-east1]';
-- Global data (billing, analytics)ALTER TABLE subscriptions CONFIGURE ZONE USING constraints = '[]';

Results:

  • Latency can reduce from 200ms to 50ms for international users
  • GDPR compliance achieved through data localization
  • Development complexity increases but operational benefits often justify the cost

Performance Benchmarking

Read Performance Comparison

Based on typical testing with 1M records (results vary by hardware and configuration):

Simple Key Lookups (ops/second):

  • Redis: 100,000+
  • DynamoDB: 50,000
  • PostgreSQL (indexed): 25,000
  • MongoDB (indexed): 20,000
  • MySQL (indexed): 22,000

Complex Queries (analytical workloads):

  • PostgreSQL: Excellent (sophisticated query planner)
  • CockroachDB: Good (distributed but still SQL)
  • MongoDB: Poor (aggregation pipelines)
  • DynamoDB: Not applicable (limited query capabilities)

Write Performance Under Load

Concurrent Writes (1000 clients):

  • DynamoDB: Scales automatically, consistent performance
  • Redis: Excellent until memory limit
  • PostgreSQL: Good with proper connection pooling
  • MongoDB: Degrades with document size growth

Implementation Patterns

Database Sharding Strategies

Horizontal Sharding (dividing data across servers):

typescript
// User-based shardingfunction getShardForUser(userId: string): string {  const hash = createHash('md5').update(userId).digest('hex');  const shardIndex = parseInt(hash.substring(0, 8), 16) % NUM_SHARDS;  return `shard_${shardIndex}`;}
// Route queries to appropriate shardclass ShardedUserService {  async getUser(userId: string) {    const shard = getShardForUser(userId);    return this.databases[shard].query('SELECT * FROM users WHERE id = ?', [userId]);  }}

Vertical Sharding (separating by feature):

typescript
// Separate databases by domainclass UserService {  profiles = new DatabaseConnection('user_profiles_db');  preferences = new DatabaseConnection('user_preferences_db');  analytics = new DatabaseConnection('user_analytics_db');    async getFullUser(userId: string) {    const [profile, preferences, analytics] = await Promise.all([      this.profiles.getUser(userId),      this.preferences.getUser(userId),      this.analytics.getUser(userId)    ]);        return { ...profile, preferences, analytics };  }}

Connection Management

PostgreSQL Connection Pooling:

typescript
// Production PostgreSQL setupimport { Pool } from 'pg';
const pool = new Pool({  host: process.env.DB_HOST,  database: process.env.DB_NAME,  user: process.env.DB_USER,  password: process.env.DB_PASSWORD,  // Critical production settings  max: 20,  // Maximum connections  idleTimeoutMillis: 30000,  // Close idle connections  connectionTimeoutMillis: 2000, // Fail fast on connection issues  maxUses: 7500,  // Rotate connections to prevent memory leaks});
// Always use transactions for data consistencyasync function transferMoney(fromUserId: string, toUserId: string, amount: number) {  const client = await pool.connect();    try {    await client.query('BEGIN');        await client.query(      'UPDATE accounts SET balance = balance - $1 WHERE user_id = $2',      [amount, fromUserId]    );        await client.query(      'UPDATE accounts SET balance = balance + $1 WHERE user_id = $2',      [amount, toUserId]    );        await client.query('COMMIT');  } catch (error) {    await client.query('ROLLBACK');    throw error;  } finally {    client.release();  }}

Monitoring and Troubleshooting

Key Metrics to Track

PostgreSQL Essential Metrics:

  • Connection usage (pg_stat_activity)
  • Query performance (pg_stat_statements)
  • Index usage (pg_stat_user_indexes)
  • Replication lag (pg_stat_replication)
sql
-- PostgreSQL health check queries-- Long-running queriesSELECT pid, now() - query_start as duration, query FROM pg_stat_activity WHERE now() - query_start > interval '5 minutes';
-- Index usage statisticsSELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetchFROM pg_stat_user_indexes ORDER BY idx_scan DESC;
-- Connection count by stateSELECT state, count(*) FROM pg_stat_activity GROUP BY state;

DynamoDB CloudWatch Metrics:

  • ConsumedReadCapacityUnits / ConsumedWriteCapacityUnits
  • ThrottledRequests (critical!)
  • SuccessfulRequestLatency
  • SystemErrors

Note: DynamoDB pricing was reduced by ~50% in November 2024, making on-demand pricing more cost-effective for variable workloads.

MongoDB Key Metrics:

  • Operations per second (opcounters)
  • Working set size vs available memory
  • Lock percentage
  • Replication lag

Common Performance Issues

The N+1 Query Problem:

typescript
// BAD: N+1 queriesasync function getUsersWithPosts() {  const users = await db.query('SELECT * FROM users');    for (const user of users) {    user.posts = await db.query('SELECT * FROM posts WHERE user_id = ?', [user.id]);  }    return users;}
// GOOD: Single query with JOINasync function getUsersWithPosts() {  return db.query(`    SELECT u.*, p.id as post_id, p.title, p.content    FROM users u    LEFT JOIN posts p ON u.id = p.user_id    ORDER BY u.id, p.created_at DESC  `);}

Connection Pool Exhaustion:

typescript
// Monitoring connection pool healthsetInterval(() => {  console.log({    totalConnections: pool.totalCount,    idleConnections: pool.idleCount,    waitingClients: pool.waitingCount  });    if (pool.waitingCount > 5) {    console.warn('Connection pool under pressure!');  }}, 30000);

Future-Proofing Your Database Choice

Vector Databases for AI/ML: pgvector for PostgreSQL, Pinecone, Weaviate

  • Embedding storage for semantic search
  • RAG (Retrieval-Augmented Generation) applications
  • Image and document similarity search

Multi-Model Databases: FaunaDB, Azure Cosmos DB

  • Single database supporting multiple data models
  • Reduced operational complexity
  • Unified query interfaces

Serverless-First Architectures:

  • PlanetScale (serverless MySQL)
  • Neon (serverless PostgreSQL)
  • FaunaDB (serverless transactional)

Planning for Growth

Capacity Planning Framework:

typescript
// Database growth projection modelinterface GrowthProjection {  currentUsers: number;  userGrowthRate: number; // monthly %  avgDataPerUser: number; // in KB  queryGrowthMultiplier: number; // queries grow faster than users}
function projectDatabaseNeeds(projection: GrowthProjection, months: number) {  const futureUsers = projection.currentUsers * Math.pow(1 + projection.userGrowthRate, months);  const futureDataSize = futureUsers * projection.avgDataPerUser;  const futureQPS = futureUsers * projection.queryGrowthMultiplier;    return {    estimatedUsers: Math.round(futureUsers),    estimatedDataSizeGB: Math.round(futureDataSize / 1024 / 1024),    estimatedQPS: Math.round(futureQPS),    recommendedShards: Math.ceil(futureQPS / 10000) // Assuming 10K QPS per shard  };}

Team Development Strategy

Skill Building Path:

  1. Foundation: Master one SQL database deeply (PostgreSQL recommended)
  2. NoSQL Understanding: Learn one document store (MongoDB) and one key-value (Redis)
  3. Cloud Native: Understand one cloud database (DynamoDB or Cosmos DB)
  4. Specialization: Deep dive into domain-specific databases (time-series, graph, etc.)

Knowledge Sharing Practices:

  • Database design reviews for all new features
  • Regular performance analysis sessions
  • Post-mortem analysis of database-related incidents
  • Cross-training on different database technologies

Decision Framework

When choosing a database for a new project, ask these questions in order:

1. Consistency Requirements

  • Do you need ACID transactions? → SQL databases
  • Can you work with eventual consistency? → NoSQL options open up

2. Query Complexity

  • Complex analytical queries? → PostgreSQL, CockroachDB
  • Simple key-value lookups? → Redis, DynamoDB
  • Full-text search required? → Elasticsearch + primary database

3. Scale and Performance

  • Current scale: <100K users → PostgreSQL + Redis
  • Growth trajectory: >1M users → Consider sharding or cloud-native options
  • Latency requirements: <10ms → In-memory (Redis) or optimized NoSQL

4. Team and Operational Constraints

  • Team expertise: Stick close to existing skills initially
  • Operational budget: Managed services vs. self-hosted
  • Compliance requirements: Data residency, encryption, audit trails

5. Future Flexibility

  • How likely is the data model to change? → Document stores for high change rate
  • Multi-region expansion planned? → Consider distributed databases early
  • Integration requirements: What other systems need to connect?

References

Related Posts