DSA Interview Prep · Lekcja

Skalowalne przechowywanie danych: SQL a NoSQL

Wybierać między relacyjnymi bazami danych, magazynami klucz-wartość, bazami dokumentowymi i magazynami szerokokolumnowymi na podstawie wzorców dostępu, wymagań dotyczących spójności i skali

Lekcja 2 z 413 kroki

Skalowalne przechowywanie danych: SQL a NoSQL to bezpłatna lekcja DSA Interview Prep na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej DSA Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.

Wybór magazynu danych to kompromis

Wybór magazynu danych jest jedną z najbardziej brzemiennych w skutki decyzji podczas projektowania systemu. Nie istnieje baza danych najlepsza w każdej sytuacji — każdy typ jest zoptymalizowany pod kątem innych wzorców dostępu, gwarancji spójności i charakterystyki skalowania. Błędny wybór w środowisku produkcyjnym prowadzi do wielu miesięcy uciążliwych prac migracyjnych.

Podczas rozmów rekrutacyjnych osoby rekrutujące sprawdzają, czy rozumieją Państwo podstawowe różnice i potrafią dopasować silnik magazynu danych do wymagań problemu. Pytanie nigdy nie brzmi: „co jest lepsze?”, lecz: „co jest lepsze dla tego konkretnego obciążenia?”. Swój wybór należy zawsze uzasadnić konkretnymi wymaganiami.

# 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}')

Bazy relacyjne (SQL): zalety

Bazy relacyjne (PostgreSQL, MySQL, SQLite) przechowują dane w tabelach o ustalonych schematach i obsługują transakcje ACID — atomowość, spójność, izolację i trwałość. Doskonale radzą sobie ze złożonymi zapytaniami obejmującymi złączenia, agregacje i filtry, dzięki czemu idealnie nadają się do uporządkowanych danych o dobrze zdefiniowanych zależnościach.

Najważniejsze zalety: złożone zapytania obejmujące wiele tabel za pomocą SQL, ograniczenia kluczy obcych zapewniające integralność danych, zaawansowane indeksowanie (B-tree, hash, pełnotekstowe), a także dojrzały ekosystem z replikacją i kopiami zapasowymi. SQL należy wybrać, gdy dane są silnie powiązane, spójność ma kluczowe znaczenie, a zapytania są złożone i różnorodne.

# 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)

Wyzwania związane ze skalowaniem SQL

Bazy danych SQL naturalnie skalują się pionowo (większy serwer, więcej CPU/RAM), ale skalowanie poziome jest złożone. Repliki odczytu obsługują obciążenia z przewagą odczytów, kierując odczyty do węzłów replik, a zapisy do węzła głównego. Przepustowość zapisu jest jednak ograniczona do jednego węzła głównego, chyba że zostanie zastosowane dzielenie na fragmenty.

Dzielenie na fragmenty (sharding) dzieli dane między wiele instancji DB na podstawie klucza fragmentu (np. user_id mod N). Zapewnia to skalowanie zapisu, ale uniemożliwia złączenia i transakcje między fragmentami — dwie najważniejsze zalety SQL. Większość aplikacji internetowych zaczyna przekraczać możliwości pojedynczego węzła SQL przy około 5–10 TB danych lub ~100K zapisów na sekundę.

# 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')

Magazyny klucz-wartość: Redis i DynamoDB

Magazyny klucz-wartość przechowują dane jako pary klucz → wartość i zapewniają odczyt oraz zapis na podstawie klucza w czasie O(1). Poświęcają elastyczność zapytań na rzecz wyjątkowej wydajności i skalowalności poziomej. Redis (działający w pamięci) osiąga opóźnienia rzędu mikrosekund, a DynamoDB (zarządzany) osiąga opóźnienia rzędu jednocyfrowej liczby milisekund i automatycznie skaluje się do dowolnej przepustowości.

Magazyny klucz-wartość należy stosować do: przechowywania sesji, buforowania, przełączników funkcji, mapowań skróconych adresów URL, koszyków zakupowych i rankingów czasu rzeczywistego. Nie należy ich stosować, gdy potrzebne są: złożone zapytania, relacje między encjami lub filtrowanie ad hoc — można wyszukiwać wyłącznie na podstawie dokładnego klucza.

# 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')

Magazyny dokumentowe: MongoDB

Magazyny dokumentowe (MongoDB, Couchbase) przechowują dane w dokumentach podobnych do JSON, co pozwala stosować elastyczne schematy — dokumenty w tej samej kolekcji mogą mieć różne pola. Obsługują indeksowanie dowolnego pola i dość zaawansowane zapytania (filtry, projekcje, agregacje), choć transakcje obejmujące wiele dokumentów są bardziej ograniczone niż w SQL.

Magazyny dokumentowe sprawdzają się w aplikacjach, w których: kształt danych różni się w zależności od encji (profile użytkowników z różnymi atrybutami), oczekiwana jest szybka ewolucja schematu (startupy często zmieniające modele danych) lub wzorce odczytu polegają głównie na pobieraniu całych encji, a nie na łączeniu tabel.

# 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')

Magazyny szerokokolumnowe: Cassandra

Magazyny szerokokolumnowe (Apache Cassandra, HBase) przechowują dane w wierszach i kolumnach, ale pozwalają, aby każdy wiersz miał inny zestaw kolumn. Są przeznaczone do obsługi ogromnej przepustowości zapisu rozproszonej między wiele węzłów, a ich domyślnym modelem jest spójność ostateczna. Cassandra zapewnia liniową skalowalność zapisu — podwojenie liczby węzłów podwaja przepustowość zapisu.

Kompromis polega na tym, że zapytania muszą być projektowane wokół klucza partycji. Nie można wydajnie filtrować ani sortować według dowolnych kolumn — najpierw trzeba zdefiniować wzorzec zapytania, a następnie zaprojektować tabelę tak, aby mu odpowiadała. Jest to przeciwieństwo podejścia SQL: „najpierw zaprojektuj dane, a potem pisz dowolne zapytania”.

# 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)')

Twierdzenie CAP: spójność, dostępność i odporność na podział sieci

Twierdzenie CAP głosi, że system rozproszony może gwarantować najwyżej dwie z następujących właściwości: Consistency (spójność — każdy odczyt zwraca wynik najnowszego zapisu), Availability (dostępność — każde żądanie otrzymuje odpowiedź niebędącą błędem) oraz Partition tolerance (odporność na podział sieci — system działa dalej pomimo podziałów sieci).

Ponieważ w systemach rozproszonych podziały sieci zawsze mogą wystąpić, rzeczywisty wybór dotyczy CP (priorytetem jest spójność, więc podczas podziału system może odrzucać żądania) oraz AP (priorytetem jest dostępność, więc system może zwracać nieaktualne dane). Bazy danych SQL są zazwyczaj typu CP, natomiast Cassandra i DynamoDB są typu AP (domyślnie zapewniają spójność ostateczną). Redis Cluster jest typu 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"]}')

Wybór magazynu danych: ramy decyzyjne

Praktyczne ramy decyzyjne dotyczące wyboru magazynu danych podczas rozmów rekrutacyjnych:

  • Potrzebne są transakcje ACID? → Relacyjna baza danych (PostgreSQL, MySQL)
  • Potrzebne są wyszukiwania w czasie krótszym niż milisekunda lub buforowanie? → Magazyn klucz-wartość (Redis)
  • Potrzebny jest elastyczny schemat lub dane zorientowane na dokumenty? → Magazyn dokumentowy (MongoDB)
  • Potrzebna jest ogromna przepustowość zapisu (>100K/s) dla danych szeregów czasowych lub zdarzeń? → Magazyn szerokokolumnowy (Cassandra)
  • Potrzebna jest globalna dystrybucja z zarządzanym skalowaniem? → DynamoDB lub Cosmos DB
  • Potrzebne są przejścia po grafie? → Grafowa baza danych (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)}')

Poliglotyczne przechowywanie danych: korzystanie z wielu magazynów

Systemy produkcyjne rzadko używają jednej bazy danych do wszystkiego. Poliglotyczne przechowywanie danych oznacza używanie właściwej technologii przechowywania dla każdej części systemu. Typowa aplikacja internetowa może używać: PostgreSQL do przechowywania głównych danych użytkowników i zamówień, Redis do przechowywania sesji i buforowania, Elasticsearch do wyszukiwania pełnotekstowego, S3 do przechowywania plików oraz Cassandry do dzienników zdarzeń i analiz.

Kompromisem jest wzrost złożoności operacyjnej wraz z każdym typem bazy danych. Zespół musi utrzymywać, monitorować i tworzyć kopie zapasowe wielu systemów. Uzasadnienie projektowe: przy dużej skali korzyści w zakresie wydajności i skalowalności wynikające z dopasowania każdego obciążenia do właściwego magazynu przewyższają koszt operacyjny.

# 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 indeksowania w różnych typach magazynów

Wszystkie systemy przechowywania danych używają indeksów, aby przyspieszyć odczyty kosztem wolniejszych zapisów i dodatkowej przestrzeni dyskowej. Zrozumienie indeksowania w różnych typach magazynów ma kluczowe znaczenie podczas projektowania systemów:

  • SQL: indeks B-tree na dowolnej kolumnie; indeksy złożone dla zapytań obejmujących wiele kolumn; indeksy pokrywające, aby uniknąć odczytów z tabeli
  • MongoDB: indeks na dowolnym polu; indeksy złożone; indeksy TTL do automatycznego wygasania wpisów
  • Cassandra: natywnie indeksowane są tylko klucz partycji i kolumny klastrowania; indeksy dodatkowe są kosztowne
  • Redis: posortowane zbiory jako indeksy zapytań zakresowych; w przypadku klucz-wartość nie są potrzebne tradycyjne indeksy
# 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 czy NoSQL podczas rozmowy rekrutacyjnej: co powiedzieć

Gdy podczas rozmowy rekrutacyjnej dotyczącej projektowania systemów padnie pytanie „SQL czy NoSQL?”, nigdy nie należy odpowiadać jednym słowem. Zamiast tego należy zastosować następującą strukturę:

  1. Przedstawić charakterystykę obciążenia: „To obciążenie jest zdominowane przez odczyty i obejmuje złożone filtrowanie, więc…”
  2. Przedstawić wymaganie: „Potrzebujemy silnej spójności dla transakcji finansowych, więc…”
  3. Przedstawić wybór: „Użyłbym PostgreSQL z replikami odczytu”
  4. Przedstawić kompromis: „Kompromis polega na tym, że poziome skalowanie zapisu wymaga dzielenia na fragmenty, co zwiększa złożoność”
  5. Wspomnieć o alternatywie: „Gdyby liczba zapisów była większa, moglibyśmy rozważyć 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')

Szybki test

Sprawdź swoją znajomość zagadnień Data Structures & Algorithms — Coding Interview Prep z tej lekcji.

Podsumowanie lekcji

W tej lekcji dowiedzieli się Państwo, że: bazy danych SQL zapewniają transakcje ACID i złożone zapytania, ale trudno skalować w nich zapisy, podczas gdy bazy danych NoSQL poświęcają spójność lub elastyczność zapytań na rzecz wyjątkowej skali i dostępności, twierdzenie CAP zmusza systemy rozproszone do wyboru między spójnością a dostępnością podczas podziałów sieci, a także systemy produkcyjne zazwyczaj korzystają z poliglotycznego przechowywania danych — dopasowują każdy typ obciążenia do właściwego silnika magazynu danych. W następnej części omówimy warstwy buforowania, CDN-y i równoważenie obciążenia, aby jeszcze bardziej skalować systemy z przewagą odczytów.

Bezpłatny start

Ucz się Python dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
30
Lekcje
120

Często zadawane pytania

Czy lekcja „Skalowalne przechowywanie danych: SQL a NoSQL” jest bezpłatna?

Tak — pełny tekst „Skalowalne przechowywanie danych: SQL a NoSQL” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu DSA Interview Prep, przejdź na CoddyKit PRO. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „Skalowalne przechowywanie danych: SQL a NoSQL”?

Wybierać między relacyjnymi bazami danych, magazynami klucz-wartość, bazami dokumentowymi i magazynami szerokokolumnowymi na podstawie wzorców dostępu, wymagań dotyczących spójności i skali Ćwiczysz DSA Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć DSA Interview Prep?

Nie wymagamy żadnego doświadczenia. DSA Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.

Ile czasu zajmuje lekcja „Skalowalne przechowywanie danych: SQL a NoSQL”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji DSA Interview Prep?

Tak. Każda lekcja DSA Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Schemat rozmowy technicznej o projektowaniu systemów
  2. Skalowalne przechowywanie danych: SQL a NoSQL
  3. Buforowanie, CDN-y i równoważenie obciążenia
  4. Projektowanie ogranicznika przepustowości i kanału Twittera
← Powrót do DSA Interview Prep