0Pricing
DSA Interview Prep · Lezione

Archiviazione scalabile dei dati: SQL e NoSQL

Scelga tra database relazionali, key-value store, database documentali e database wide-column in base ai pattern di accesso, ai requisiti di consistenza e alla scalabilità richiesta.

Archiviazione scalabile dei dati: SQL e NoSQL è una lezione DSA Interview Prep gratuita su CoddyKit. Questa è la lezione 2 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DSA Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DSA Interview Prep include 4 lezioni in totale.

La scelta dello storage è un compromesso

Scegliere un data store è una delle decisioni più determinanti nella progettazione dei sistemi. Nessun database è il migliore in assoluto: ogni tipo è ottimizzato per diversi pattern di accesso, garanzie di consistenza e caratteristiche di scalabilità. Sbagliare questa scelta in produzione può causare mesi di complesse migrazioni.

Nei colloqui, gli intervistatori verificano se lei comprende le differenze fondamentali e sa associare un motore di storage ai requisiti di un problema. La domanda non è mai 'qual è il migliore?', ma 'qual è il migliore per questo carico di lavoro specifico?'. Giustifichi sempre la scelta sulla base di requisiti specifici.

# Storage decision matrix summary
factors = [
    'Data structure (tabular, documents, key-value, graph, time-series)',
    'Read vs write ratio (read-heavy, write-heavy, balanced)',
    'Query patterns (point lookups, range scans, aggregations, joins)',
    'Consistency requirements (ACID vs eventual consistency)',
    'Scale requirements (single node, sharding, global distribution)',
    'Latency requirements (milliseconds vs microseconds)',
    'Team familiarity and operational complexity',
]
print('Key factors for storage selection:')
for f in factors:
    print(f'  - {f}')

Database relazionali (SQL): punti di forza

I database relazionali (PostgreSQL, MySQL, SQLite) memorizzano i dati in tabelle con schemi fissi e supportano le transazioni ACID — Atomicità, Consistenza, Isolamento, Durabilità. Sono eccellenti per query complesse con join, aggregazioni e filtri, il che li rende ideali per dati strutturati con relazioni ben definite.

Principali punti di forza: query complesse su più tabelle tramite SQL, vincoli di chiave esterna per l’integrità dei dati, potenti sistemi di indicizzazione (B-tree, hash, full-text), ecosistema maturo con replica e backup. Usi SQL quando i dati sono altamente relazionali, la consistenza è fondamentale e le query sono complesse e diversificate.

# SQL excels at: complex queries, joins, transactions

# Example: find top 5 products by revenue this month
sql_query = '''
SELECT p.name, SUM(oi.quantity * oi.price) AS revenue
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.created_at >= DATE_TRUNC('month', NOW())
GROUP BY p.id, p.name
ORDER BY revenue DESC
LIMIT 5;
'''
print('SQL shines for relational queries with JOINs:')
print(sql_query)

print('ACID guarantees example (transfer $100 between accounts):')
transfer_sql = '''
BEGIN;
  UPDATE accounts SET balance = balance - 100 WHERE id = 1;
  UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;  -- either both succeed or neither does
'''  
print(transfer_sql)

Difficoltà di scalabilità di SQL

I database SQL scalano verticalmente in modo naturale (server più potente, più CPU/RAM), ma la scalabilità orizzontale è complessa. Le repliche di lettura gestiscono i carichi di lavoro con molte letture instradando le letture ai nodi replica e le scritture al nodo primario. Tuttavia, il throughput di scrittura è limitato a un singolo nodo primario, a meno di aggiungere lo sharding.

Lo sharding partiziona i dati tra più istanze DB usando una shard key (ad esempio, `user_id mod N`). In questo modo aumenta la scalabilità delle scritture, ma compromette i join e le transazioni tra shard — due delle funzionalità più importanti di SQL. La maggior parte delle applicazioni web supera i limiti di SQL su un singolo nodo intorno a 5-10 TB di dati o ~100K scritture al secondo.

# SQL scaling strategies
strategies = {
    'Read replicas': {
        'how': 'One primary (writes), multiple replicas (reads)',
        'scales': 'Read throughput (10x+)',
        'limit': 'Write throughput still bounded by single primary',
    },
    'Connection pooling (PgBouncer)': {
        'how': 'Pool of persistent DB connections shared among app servers',
        'scales': 'Connection count (PostgreSQL max ~500 connections)',
        'limit': 'Does not increase query throughput',
    },
    'Horizontal sharding': {
        'how': 'Partition rows by shard key across N database instances',
        'scales': 'Both reads and writes (N×)',
        'limit': 'Cross-shard joins and transactions broken; complex routing',
    },
    'CQRS': {
        'how': 'Separate write model (SQL) from read model (denormalised/NoSQL)',
        'scales': 'Optimise each path independently',
        'limit': 'Eventual consistency between write and read models',
    },
}
for strategy, info in strategies.items():
    print(f'{strategy}:\n  How: {info["how"]}\n  Scales: {info["scales"]}\n  Limit: {info["limit"]}\n')

Database chiave-valore: Redis e DynamoDB

I database chiave-valore memorizzano i dati come coppie chiave → valore, con letture e scritture O(1) sulla chiave. Rinunciano alla flessibilità delle query in favore di prestazioni estreme e scalabilità orizzontale. Redis (in memoria) raggiunge latenze nell’ordine dei microsecondi; DynamoDB (gestito) raggiunge latenze di pochi millisecondi con scalabilità automatica per qualsiasi throughput.

Usi i database chiave-valore per: archiviazione delle sessioni, caching, feature flag, associazioni per l’abbreviazione degli URL, carrelli della spesa e classifiche in tempo reale. Non li utilizzi quando servono: query complesse, relazioni tra entità o filtri ad hoc — è possibile eseguire ricerche solo tramite una chiave esatta.

# Key-value store use cases
kv_operations = {
    'GET key':        'O(1) point lookup — the core operation',
    'SET key value':  'O(1) insert or update',
    'DEL key':        'O(1) delete',
    'EXPIRE key ttl': 'Set time-to-live; key auto-deleted after ttl seconds',
    'INCR key':       'Atomic increment — useful for counters and rate limiting',
    'LPUSH/LRANGE':   'List operations — useful for queues and recent-items feeds',
    'ZADD/ZRANGE':    'Sorted set — leaderboards, rate limiting with sliding window',
}
print('Redis operation set:')
for op, desc in kv_operations.items():
    print(f'  {op:25s}: {desc}')

print('\nDynamoDB vs Redis:')
print('  Redis:    microsecond latency, in-memory, needs persistence config')
print('  DynamoDB: single-digit ms, managed, auto-scaling, durable by default')

Database documentali: MongoDB

I database documentali (MongoDB, Couchbase) memorizzano i dati come documenti simili a JSON, consentendo schemi flessibili: i campi possono differire tra documenti della stessa raccolta. Supportano indici su qualunque campo e query abbastanza ricche (filtri, proiezioni, aggregazioni), sebbene le transazioni su più documenti siano più limitate rispetto a SQL.

I database documentali sono adatti alle applicazioni in cui la struttura dei dati varia per entità (profili utente con attributi diversi), è prevista una rapida evoluzione dello schema (startup che modificano frequentemente i modelli dati) oppure i pattern di lettura consistono soprattutto nel recuperare intere entità anziché eseguire join tra tabelle.

# MongoDB document example
user_doc = {
    '_id': 'user123',
    'name': 'Alice',
    'email': 'alice@example.com',
    'preferences': {
        'theme': 'dark',
        'language': 'en',
        'notifications': ['email', 'push']
    },
    'addresses': [
        {'type': 'home', 'city': 'Berlin', 'country': 'DE'},
        {'type': 'work', 'city': 'Munich', 'country': 'DE'}
    ],
    'subscription_tier': 'pro',
    # Note: not all users have all fields -- flexible schema!
}

import json
print('Document structure (flexible schema):')
print(json.dumps(user_doc, indent=2))

print('\nDocument store strengths:')
print('  - Nested/array fields without joins')
print('  - Flexible schema (different fields per document)')
print('  - Scales horizontally by sharding on _id')

Database a colonne larghe: Cassandra

I database a colonne larghe (Apache Cassandra, HBase) memorizzano i dati in righe e colonne, ma permettono a ogni riga di avere un insieme diverso di colonne. Sono progettati per un throughput di scrittura enorme, distribuito tra molti nodi, con consistenza eventuale per impostazione predefinita. Cassandra offre una scalabilità lineare delle scritture: raddoppiando i nodi raddoppia il throughput di scrittura.

Il compromesso è che le query devono essere progettate in funzione della partition key. Non è possibile filtrare o ordinare in modo efficiente per colonne arbitrarie: prima bisogna definire il pattern di query e poi progettare la tabella di conseguenza. È l’opposto dell’approccio di SQL: 'progettare i dati e poi scrivere qualsiasi query'.

# Cassandra table design for time-series events
# Design around the query: 'give me all events for user X, most recent first'

cassandra_table = '''
CREATE TABLE user_events (
    user_id    UUID,
    event_time TIMESTAMP,
    event_type TEXT,
    metadata   MAP<TEXT, TEXT>,
    PRIMARY KEY (user_id, event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);

-- Query (matches partition key exactly):
SELECT * FROM user_events WHERE user_id = ? LIMIT 100;
'''
print('Cassandra wide-column design:')
print(cassandra_table)
print('Properties:')
print('  - user_id = partition key (all rows for one user on same node)')
print('  - event_time = clustering key (sorted within partition)')
print('  - Very fast writes: append-only, no locking')
print('  - Cannot query by event_type alone (no partition key)')

Teorema CAP: consistenza, disponibilità e tolleranza al partizionamento

Il teorema CAP afferma che un sistema distribuito può garantire al massimo due tra: Consistenza (ogni lettura restituisce l’ultima scrittura), Availability (ogni richiesta riceve una risposta che non è un errore) e Partition tolerance (il sistema continua a funzionare nonostante le partizioni di rete).

Poiché le partizioni di rete si verificano sempre nei sistemi distribuiti, la scelta reale è tra CP (privilegiare la consistenza, con la possibilità di rifiutare richieste durante una partizione) e AP (privilegiare la disponibilità, con la possibilità di restituire dati non aggiornati). I database SQL sono generalmente CP; Cassandra e DynamoDB sono AP (consistenza eventuale per impostazione predefinita). Redis Cluster è CP.

# CAP theorem applied to common databases
databases = {
    'PostgreSQL (single node)': {'C': True,  'A': True,  'P': False, 'note': 'Not distributed; CA'},
    'PostgreSQL (multi-AZ)':    {'C': True,  'A': False, 'P': True,  'note': 'CP: primary fails over, brief downtime'},
    'MySQL Cluster':            {'C': True,  'A': False, 'P': True,  'note': 'CP'},
    'Cassandra':                {'C': False, 'A': True,  'P': True,  'note': 'AP: eventual consistency default'},
    'DynamoDB (default)':       {'C': False, 'A': True,  'P': True,  'note': 'AP: eventual consistency'},
    'DynamoDB (strong read)':   {'C': True,  'A': False, 'P': True,  'note': 'CP: strongly consistent reads'},
    'Redis Cluster':            {'C': True,  'A': False, 'P': True,  'note': 'CP'},
    'MongoDB (default)':        {'C': True,  'A': False, 'P': True,  'note': 'CP: reads from primary'},
}
for db, caps in databases.items():
    c_str = 'C' if caps['C'] else '-'
    a_str = 'A' if caps['A'] else '-'
    p_str = 'P' if caps['P'] else '-'
    print(f'{db:35s} [{c_str}{a_str}{p_str}] {caps["note"]}')

Scegliere lo storage: un framework decisionale

Un framework pratico per scegliere lo storage durante i colloqui:

  • Ha bisogno di transazioni ACID? → Database relazionale (PostgreSQL, MySQL)
  • Ha bisogno di ricerche in meno di un millisecondo o di caching? → Database chiave-valore (Redis)
  • Ha bisogno di uno schema flessibile o di dati orientati ai documenti? → Database documentale (MongoDB)
  • Ha bisogno di un throughput di scrittura enorme (>100K/sec) con dati di serie temporali o eventi? → Database a colonne larghe (Cassandra)
  • Ha bisogno di una distribuzione globale con scalabilità gestita? → DynamoDB o Cosmos DB
  • Ha bisogno di attraversamenti di grafi? → Database a grafo (Neo4j)
# Decision tree in code form
def choose_storage(needs_acid, high_write_throughput, flexible_schema,
                   sub_ms_latency, graph_queries, global_scale):
    if sub_ms_latency:
        return 'Redis (in-memory key-value)'
    if graph_queries:
        return 'Neo4j (graph database)'
    if needs_acid:
        return 'PostgreSQL / MySQL (relational)'
    if high_write_throughput and not flexible_schema:
        return 'Cassandra (wide-column, write-optimised)'
    if flexible_schema:
        return 'MongoDB (document store)'
    if global_scale:
        return 'DynamoDB or Cosmos DB (managed global KV/document)'
    return 'PostgreSQL (safe default for most web apps)'

# Example scenarios
scenarios = [
    {'needs_acid': True,  'high_write_throughput': False, 'flexible_schema': False,
     'sub_ms_latency': False, 'graph_queries': False, 'global_scale': False},
    {'needs_acid': False, 'high_write_throughput': True,  'flexible_schema': False,
     'sub_ms_latency': False, 'graph_queries': False, 'global_scale': False},
    {'needs_acid': False, 'high_write_throughput': False, 'flexible_schema': False,
     'sub_ms_latency': True,  'graph_queries': False, 'global_scale': False},
]
for s in scenarios:
    print(f'{choose_storage(**s)}')

Persistenza poliglotta: utilizzo di più sistemi di storage

I sistemi in produzione raramente usano un solo database per ogni esigenza. La persistenza poliglotta consiste nell’utilizzare la tecnologia di storage giusta per ogni parte del sistema. Una tipica applicazione web potrebbe usare: PostgreSQL per i dati principali degli utenti e degli ordini, Redis per l’archiviazione delle sessioni e il caching, Elasticsearch per la ricerca full-text, S3 per l’archiviazione dei file e Cassandra per i log degli eventi e l’analisi.

Il compromesso è che la complessità operativa aumenta con ogni tipo di database. Il team deve gestire, monitorare ed eseguire il backup di più sistemi. Motivazione progettuale: su larga scala, i vantaggi in termini di prestazioni e scalabilità derivanti dall’associare ogni carico di lavoro allo storage giusto compensano il costo operativo.

# Polyglot persistence in an e-commerce system
components = {
    'User accounts, orders, payments': {
        'storage': 'PostgreSQL',
        'reason': 'ACID transactions (payment integrity), complex queries',
    },
    'Product catalogue': {
        'storage': 'MongoDB or PostgreSQL with JSONB',
        'reason': 'Flexible product attributes vary by category',
    },
    'Session tokens': {
        'storage': 'Redis (with TTL)',
        'reason': 'O(1) lookup, automatic expiry, high throughput',
    },
    'Product search': {
        'storage': 'Elasticsearch',
        'reason': 'Full-text search, faceted filtering, relevance scoring',
    },
    'Activity/event log': {
        'storage': 'Cassandra or Kafka + S3',
        'reason': 'High write throughput, append-only, time-series queries',
    },
    'Product images / videos': {
        'storage': 'S3 + CloudFront CDN',
        'reason': 'Cheap object storage, global distribution via CDN',
    },
}
for component, info in components.items():
    print(f'{component}:\n  {info["storage"]}: {info["reason"]}\n')

Strategia di indicizzazione tra i diversi tipi di storage

Tutti i sistemi di storage utilizzano gli indici per velocizzare le letture, a discapito di scritture più lente e di maggiore spazio di archiviazione. Comprendere l’indicizzazione nei diversi tipi di storage è fondamentale per la progettazione dei sistemi:

  • SQL: indice B-tree su qualsiasi colonna; indici composti per query su più colonne; indici covering per evitare accessi alla tabella
  • MongoDB: indice su qualsiasi campo; indici composti; indici TTL per la scadenza automatica
  • Cassandra: solo partition key e clustering columns sono indicizzate nativamente; gli indici secondari sono costosi
  • Redis: gli sorted set come indici per query su intervalli; per il modello chiave-valore non servono indici tradizionali
# Indexing examples across storage types

# PostgreSQL: B-tree composite index
postgres_index = '''
CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at DESC);
-- Optimises: SELECT * FROM orders WHERE user_id=? ORDER BY created_at DESC
'''

# MongoDB: compound index
mongo_index = '''
db.products.createIndex({ category: 1, price: -1 })
// Optimises: db.products.find({category:'Electronics'}).sort({price:-1})
'''

# Cassandra: cluster key ordering (built into table design)
cassandra_index = '''
-- No separate index needed; clustering key IS the index:
PRIMARY KEY (user_id, event_time) WITH CLUSTERING ORDER BY (event_time DESC)
'''

print('PostgreSQL:', postgres_index)
print('MongoDB:', mongo_index)
print('Cassandra:', cassandra_index)

SQL vs NoSQL nei colloqui: cosa dire

Quando in un colloquio di system design le viene chiesto 'SQL o NoSQL?', non risponda mai con una sola parola. Segua invece questa struttura:

  1. Indichi il carico di lavoro: 'Il carico è prevalentemente in lettura e prevede filtri complessi, quindi…'
  2. Indichi il requisito: 'Ci serve una consistenza forte per le transazioni finanziarie, quindi…'
  3. Indichi la scelta: 'Userei PostgreSQL con repliche di lettura'
  4. Indichi il compromesso: 'Il compromesso è che la scalabilità orizzontale delle scritture richiede lo sharding, che aggiunge complessità'
  5. Citi un’alternativa: 'Se le scritture fossero più numerose, potremmo valutare DynamoDB'
# Sample answer structure for 'SQL or NoSQL?'
def answer_storage_question(workload, consistency_need, scale):
    print(f'Workload: {workload}')
    print(f'Consistency: {consistency_need}')
    print(f'Scale: {scale}')
    print()
    if 'financial' in workload.lower() or consistency_need == 'strong':
        choice = 'PostgreSQL (ACID, strong consistency)'
        tradeoff = 'Horizontal write scaling requires sharding'
        alternative = 'Google Spanner for global transactions'
    elif 'event' in workload.lower() or 'log' in workload.lower():
        choice = 'Cassandra (high write throughput, time-series)'
        tradeoff = 'Eventual consistency; queries limited to partition key'
        alternative = 'Kafka + S3 for long-term event archival'
    else:
        choice = 'DynamoDB (managed, auto-scale, low latency)'
        tradeoff = 'Limited query flexibility; cross-item transactions limited'
        alternative = 'PostgreSQL if complex queries emerge'
    print(f'Choice: {choice}\nTrade-off: {tradeoff}\nAlternative: {alternative}')

answer_storage_question('Social media feed', 'eventual', '10M users')

Verifica rapida

Metta alla prova la sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep affrontati in questa lezione.

Riepilogo della lezione

In questa lezione ha imparato: i database SQL offrono transazioni ACID e query complesse, ma scalano con difficoltà sul piano delle scritture, mentre i database NoSQL sacrificano la consistenza o la flessibilità delle query in favore di una scalabilità e disponibilità estreme, il teorema CAP costringe i sistemi distribuiti a scegliere tra consistenza e disponibilità durante le partizioni di rete e i sistemi in produzione usano in genere la persistenza poliglotta, associando ogni carico di lavoro al motore di storage più adatto. Nella prossima lezione esploreremo i livelli di caching, le CDN e il bilanciamento del carico per aumentare ulteriormente la scalabilità dei sistemi con molte letture.

Domande Frequenti

La lezione «Archiviazione scalabile dei dati: SQL e NoSQL» è gratuita?

Sì — il testo completo di «Archiviazione scalabile dei dati: SQL e NoSQL» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DSA Interview Prep, passa a CoddyKit PRO. Il corso DSA Interview Prep include 4 lezioni in totale.

Cosa imparerò in «Archiviazione scalabile dei dati: SQL e NoSQL»?

Scelga tra database relazionali, key-value store, database documentali e database wide-column in base ai pattern di accesso, ai requisiti di consistenza e alla scalabilità richiesta. Eserciti DSA Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare DSA Interview Prep?

Non è richiesta alcuna esperienza precedente. DSA Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 4.

Quanto tempo richiede la lezione «Archiviazione scalabile dei dati: SQL e NoSQL»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione DSA Interview Prep?

Sì. Ogni lezione DSA Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Il framework per i colloqui di system design
  2. Archiviazione scalabile dei dati: SQL e NoSQL
  3. Caching, CDN e bilanciamento del carico
  4. Progettare un rate limiter e un feed di Twitter
← Torna a DSA Interview Prep