Data Modeling & Database Fundamentals
From raw data to wisdom — relational theory, columnar internals, NoSQL paradigms, graph databases, and choosing the right model for the job.
1. Data Hierarchy
Data is raw facts. Information is data with context — organized through data models using SQL and query languages. Knowledge emerges when information connects and patterns appear. Wisdom is applying knowledge to make sound decisions.
2. Relational Foundations & ACID
Edgar Codd (1970) introduced the relational data model: data organized into relations (tables) with rows and columns. Each column attribute must have a unique name and every value in a column must be the same type — no implicit conversions ("65" - 5 + 10 is not valid).
ACID Guarantees
ACID guarantees ensure reliable concurrent database operations:
| Property | Guarantee | What it prevents |
|---|---|---|
| Atomicity | Transactions are all-or-nothing — no partial updates | Half-written records, orphaned state |
| Consistency | Data must follow defined rules (keys, types, constraints) | Invalid data entering the system |
| Isolation | Concurrent operations behave as if executed one at a time | Dirty reads, phantom reads, write conflicts |
| Durability | Once committed, data persists even after failure | Data loss on crash or power failure |
psycopg2 is the Python library for PostgreSQL with full transaction support. Jupyter's %%sql cell magic supports simple queries but does not support transactions — use psycopg2 for granular control.3. Normalization (1NF → 3NF)
Normalization is the process of organizing tables into "normal forms" to reduce redundancy and inconsistencies. Anomalies result from poor schema design — CRUD (Create, Read, Update, Delete) operations suffer when data is denormalized incorrectly.
First Normal Form (1NF)
Each cell in a table contains a single value — no arrays, no comma-separated lists, no nested structures. When a schema fails 1NF: queries become harder, optimization features (indexes) are much harder to build and use, and constraints become weaker.
Second Normal Form (2NF)
Satisfies 1NF, plus: define a clear primary key AND ensure all columns depend on the entire key, not just part of it. Relevant when using composite primary keys (two or more columns forming the key). Natural primary key: a single column like Name that identifies the record. Composite primary key: multiple columns together (e.g., StudentID + CourseID).
Third Normal Form (3NF)
Satisfies 1NF and 2NF, plus: no non-key column should depend on another non-key column (no transitive dependencies).
Benefits of Normalization
- No data duplication
- Clear dependencies between tables
- Fewer update errors and anomalies
- Well-defined keys with indexes
- Efficient OLTP writes
When to Denormalize
- OLAP / analytical queries need fast reads
- Star and Snowflake schemas for data warehouses
- Read-heavy workloads where JOINs are expensive
- Reporting and BI dashboards
- Materialized views and pre-aggregated tables
4. OLTP vs OLAP
OLTP — Row-Oriented
Each row inserted independently in sequence. Fast individual inserts and reads without impacting the rest of the table.
- Row-by-row storage on disk
- Handles concurrent transactions effectively
- Normalized schema (3NF)
- PostgreSQL, MySQL, Aurora, SQL Server
OLAP — Column-Oriented
Data stored by column. Aggregating an entire column is fast because it reads only that column without touching others.
- Columnar storage on disk
- Fast aggregations (SUM, AVG, COUNT)
- Drawback: adding a new row requires opening all columns
- Redshift, BigQuery, Snowflake, DuckDB
TPC-C for transactional (OLTP) workloads, TPC-H for analytical (OLAP) queries. Standard datasets for comparing database performance.5. Star & Snowflake Schemas
OLAP databases use denormalized schemas optimized for analytical queries. The two dominant patterns:
Star Schema
Central fact table (transactions, events) surrounded by dimension tables (who, what, when, where). Dimensions connect directly to the fact table — one level of JOIN. Simple, fast, preferred for most data warehouses.
Snowflake Schema
Like Star, but dimension tables are further normalized into sub-dimensions. More JOINs required, more storage-efficient, harder to query. Used when dimension tables are very large or shared across fact tables.
6. Columnar Storage Internals
Traditional vs True Column-Store
How Columnar Query Execution Works (Bitmap Scanning)
-- Executing: SELECT * FROM orders WHERE status='shipped' AND region='US' Step 1: Scan status column → produce position bitmap [0, 0, 1, 0, 1, 1, 0, 0, 0, 0] -- 1 = matches 'shipped' Step 2: Scan region column → produce position bitmap [1, 0, 1, 1, 0, 1, 0, 1, 0, 0] -- 1 = matches 'US' Step 3: AND bitmaps together → combined position bitmap [0, 0, 1, 0, 0, 1, 0, 0, 0, 0] -- positions 3 and 6 match both Step 4: Use bitmap to fetch only matching rows from remaining columns -- Read only 2 rows instead of 10 — massive I/O savings
Apache Parquet — Columnar but Row-Grouped
-- Parquet file structure: row-groups containing column chunks Row Group 1 (rows 0–99,999): ├── Column Chunk: order_id (100K values, compressed) ├── Column Chunk: status (100K values, compressed) └── Column Chunk: amount (100K values, compressed) Row Group 2 (rows 100K–199,999): ├── Column Chunk: order_id (100K values, compressed) ├── Column Chunk: status (100K values, compressed) └── Column Chunk: amount (100K values, compressed)
7. OLAP Database Landscape
MPP (Massively Parallel)
- Vertica
- Greenplum
- Teradata
Distribute queries across many nodes. Best for large enterprise warehouses.
Real-time OLAP
- ClickHouse
- Apache Druid
- Apache Pinot
Sub-second analytics on streaming data. Used for dashboards and monitoring.
Cloud Data Warehouses
- Amazon Redshift
- Snowflake
- Google BigQuery
Managed, elastic, pay-per-query. Dominant in modern data stacks.
8. DuckDB vs PostgreSQL
DuckDB
- Not built for fast simultaneous writes
- Stores data temporarily in volatile memory
- Must configure persistent settings to write to disk
- Limited to single-writer scenarios
- Doesn't support concurrent writes
- Ideal: analytics, Jupyter notebooks, local OLAP
PostgreSQL
- Handles concurrent transactions effectively
- Supports concurrent connections for reads and writes
- Slightly faster for loading data into tables
- Full ACID with MVCC
- Ideal: production OLTP, multi-user apps
9. NoSQL & Document Stores
NoSQL = Not Only SQL (not "no relational databases"). The structure of data keeps shifting — documents, key-value pairs, flexible record structures. Categories:
Document Stores
Rich structure, sorting, filtering, indexing. Records look like JSON objects.
MongoDB, DynamoDB, Firestore, DocumentDB
Key-Value Stores
Minimal structure — a pile of boxes. Fast simple lookups but no sorting or filtering support.
Redis, DynamoDB (KV mode), Memcached
MongoDB
"Humongous database" — built for massive scale. No schema changes needed, no table extensions. Uses MQL (MongoDB Query Language). Internally stores data as BSON (binary JSON); for users it behaves like JSON.
| MongoDB | Relational DB |
|---|---|
| Collection | Table |
| Document | Row |
| Field | Column |
Amazon DynamoDB
Purpose-built for unpredictable, massive scale — designed to support Amazon's holiday traffic spikes. Single-digit ms performance at any scale. Access patterns are more constrained than MongoDB — must design partition/sort keys carefully.
Apache Cassandra
Built for Facebook's inbox search serving millions of users. If you don't enforce some structure, it becomes extremely complex to filter and aggregate data. Amazon Keyspaces provides managed Cassandra using CQL for high-throughput workloads.
10. Document CRUD with Code
Create (Insert)
# MongoDB db.customers.insert_one(data_dict) # DynamoDB db.Table("Customers").put_item(Item=data_dict) # Firestore db.collection("Customers").add(data_dict)
Read (Query)
filter_expr = {"email": "example@email.com"}
# MongoDB
db.customers.find_one(filter_expr)
# DynamoDB
db.Table("Customers").get_item(Key=filter_expr)
# Firestore
db.collection("Customers").where("email", "==", "example@email.com").get()
Update
filter_expr = {"email": "old@email.com"}
new_values = {"email": "new@email.com"}
# MongoDB — does NOT overwrite whole doc, only updates specified attributes
db.customers.update_one(filter_expr, {"$set": new_values})
# DynamoDB — uses expression syntax, distinguishable via placeholders
db.Table("Customers").update_item(
Key=filter_expr,
UpdateExpression="SET email = :new_email",
ExpressionAttributeValues={":new_email": "new@email.com"}
)
# Firestore
result = db.collection("Customers").where("email", "==", "old").get()[0].reference
result.update(new_values)
Delete
filter_expr = {"user_id": "user123"}
# MongoDB — can delete based on ANY filter
db.customers.delete_one(filter_expr)
# DynamoDB — can ONLY delete based on primary key
db.Table("Customers").delete_item(Key=filter_expr)
# Firestore
result = db.collection("Customers").where("user_id", "==", "user123").get()[0].reference
result.delete()
11. MQL Operators & Aggregation Pipeline
MQL Update Operators
In MQL, operators always start with $ (dollar sign):
| Operator | Action | Example |
|---|---|---|
$set | Change or add a field | {"$set": {"status": "active"}} |
$unset | Remove a field | {"$unset": {"temp_field": ""}} |
$push | Append to an array | {"$push": {"tags": "new_tag"}} |
$inc | Increment a number | {"$inc": {"login_count": 1}} |
$pull | Remove from an array | {"$pull": {"tags": "old_tag"}} |
Aggregation Pipeline
MongoDB's aggregation pipeline is essentially an ETL pipeline for performing advanced data analysis and manipulation on collections:
# An ETL pipeline inside MongoDB db.orders.aggregate([ {"$lookup": {...}}, # JOIN-like operator across collections (Extract) {"$match": {...}}, # Filter documents (Extract) {"$group": { # Aggregate: $sum, $avg, $count (Transform) "_id": "$category", "total": {"$sum": "$amount"} }}, {"$sort": {...}} # Order results (Load) ])
Joins Across Document Databases
| Database | JOIN Support |
|---|---|
| Relational (SQL) | Native JOIN — first-class feature |
| MongoDB | $lookup in aggregation pipeline — JOIN-like capability |
| DynamoDB & Firestore | No support — application must handle with custom logic |
12. Schema Evolution in Document DBs
Document databases have no DDL (no CREATE TABLE, no ALTER TABLE). You simply start inserting documents. The document shape becomes the de facto structure of the collection.
MongoDB Schema Validation
MongoDB does support optional schema validation at the collection level — rules defined during collection creation validate documents on insert/update. This provides a middle ground between fully rigid relational schemas and zero enforcement.
# Define validation rules when creating a collection db.create_collection("customers", validator={ "$jsonSchema": { "required": ["name", "email"], "properties": { "name": {"bsonType": "string"}, "email": {"bsonType": "string", "pattern": "@"} } } })
13. ACID Guarantees in NoSQL
NoSQL databases implement ACID differently — scoped differently, made optional, and optimized for their specific design and workload patterns.
| Property | MongoDB | DynamoDB | Cassandra |
|---|---|---|---|
| Atomicity | At document level | At item level | At partition/row level |
| Consistency | Developer responsibility to update dependencies | Eventual or strong (configurable) | Tunable consistency (ONE, QUORUM, ALL) |
| Isolation | Document-level; multi-doc via snapshot isolation | Item-level | Partition-level |
| Durability | Document and transaction level; horizontal replication and sharding | Item-level; automatic replication across AZs | Partition-level; configurable replication factor |
14. Embedding vs Referencing
MongoDB inverts the relational pattern:
| Workload | Relational DB | MongoDB |
|---|---|---|
| OLTP | Normalized (prevent anomalies) | Denormalized — Embedding |
| OLAP | Denormalized (Star/Snowflake) | Normalized — Referencing |
Embedding (Denormalized)
Store related data inside a single document. One read retrieves everything. Best for data accessed together frequently.
customer = {
_id: 1,
name: "Alice",
address: { ← embedded
city: "Phoenix",
zip: "85001"
}
}
Referencing (Normalized)
Store a reference ID linking to another collection. Requires additional query. Best for many-to-many or frequently updated related data.
customer = {
_id: 1,
name: "Alice",
address_id: 42 ← reference
}
15. CRUD Anomalies in NoSQL
16. Graph Databases
When the most important thing about data is the relationships between items — the connections — graph databases are the natural fit. Connections are primary, not secondary. Graph databases treat connections between entities as first-class citizens.
Three questions a graph database answers naturally:
Origin & Use Cases
Neo4j was built by a team trying to build an ECM (Enterprise Content Management) system — for storing, organizing, and retrieving large amounts of interconnected content (documents, references, permissions, pages). They realized the relationships mattered more than the data itself. Graph databases provide full ACID guarantees.
Discovery & Intelligence
- Knowledge Bases
- Recommendation Engines
- Semantic Search Engines
Security & Operations
- Fraud Detection & Transaction Analysis
- Access Control & Permissions
- Social & Communication Networks
- Network Topology
Graph Data Model
| Concept | Graph DB | Relational DB | Document DB |
|---|---|---|---|
| Entity | Node | Row | Document |
| Attribute | Property | Column | Field |
| Classification | Label | Table | Collection |
| Connection | Relationship (Edge) | JOIN | Embedded / $lookup |
ACID in Graph Databases
| Property | Graph DB Implementation |
|---|---|
| Atomicity | Atomic at individual operations. Multi-step: BEGIN → create node → create relationship → COMMIT |
| Consistency | Rules optional but can be enforced via GQL schema constraints |
| Isolation | Individual updates isolated; group transactions safely handled; conflicts managed without corrupting graph |
| Durability | Written to disk. Horizontal scaling via clusters and replication |
Intermediate Nodes & Normalization
When a relationship carries too many properties, create an intermediate node instead. This is the graph equivalent of normalization — determining the right level of detail per entity.
Neo4j Python Driver: CREATE vs MERGE
# CREATE — always creates new (duplicates if run twice) driver.execute_query("CREATE (c:Customer {name: $name})", name="Alice") # MERGE — find-or-create (idempotent, preferred) driver.execute_query(""" MERGE (c:Customer {id: $id}) ON CREATE SET c.name = $name, c.created = datetime() ON MATCH SET c.updated = datetime() RETURN c """, id="C001", name="Alice") # driver.execute_query() — handles session internally (newer API) # session.run() — requires explicit with driver.session() block # RETURN is optional — use for confirmation, debugging, accessing IDs # Relationships — create with nodes or separately driver.execute_query(""" MATCH (c:Customer {id: $cid}), (e:Email {address: $email}) MERGE (c)-[:HAS_EMAIL]->(e) """, cid="C001", email="alice@example.com")
Graph Database Engines
networkx provides graph structures and algorithms without a database — useful for prototyping and analysis.17. Cloud Database Services
AWS Authentication Pattern (boto3)
import boto3, json from botocore.exceptions import ClientError # STS Session session = boto3.Session( aws_access_key_id="...", aws_secret_access_key="...", aws_session_token="...", region_name="us-east-1" ) sts_client = session.client("sts") identity = sts_client.get_caller_identity() # Secrets Manager — store DB credentials secretsmanager = boto3.client("secretsmanager") try: response = secretsmanager.create_secret( Name="rds-postgres-credentials", SecretString=json.dumps(secret_dict) ) except ClientError as e: print(e)
The Data Model — Final Thought
A data model is a structured representation connecting human understanding (logical) with machine storage and retrieval (physical).
🎯 Topics to Explore Next
Based on Data Engineering certifications and interview patterns, these topics aren't yet covered in your notes:
- Data Partitioning — hash vs range partitioning, hot spots, consistent hashing, DynamoDB partition keys
- Replication Models — leader/follower, leaderless (Dynamo-style), multi-leader, conflict resolution
- CAP Theorem Deep Dive — CP vs AP trade-offs, PACELC extension, real-world examples
- Data Lake vs Warehouse vs Lakehouse — S3 data lake, Redshift, Delta Lake/Iceberg, when to use each
- Change Data Capture (CDC) — DMS, Debezium, DynamoDB Streams, event sourcing patterns
- Streaming vs Batch — Lambda architecture, Kappa architecture, Kinesis vs Kafka
- Data Quality & Governance — Great Expectations, Glue Data Quality, Lake Formation, data lineage
- Indexing Strategies — B-tree vs LSM-tree, covering indexes, composite indexes, index-only scans
- Transaction Isolation Levels — Read Committed, Repeatable Read, Serializable, MVCC internals
- Slowly Changing Dimensions — SCD Type 1/2/3, surrogate keys, effective dates in warehouses