Why Programs Need Databases
Where files stop being enough
In this lesson
Unit 7 ended on an awkward note: your script fetched a pile of data, printed it, and exited, taking the data with it. Writing it to a file is the obvious fix, and for a while it is the right one. This lesson is about the point where it stops being right, and what a database gives you instead.
Explain it like I’m 5
A text file is a shoebox of receipts. A database is the same receipts in a filing cabinet with labeled drawers you can actually search.
Files are fine, until they aren’t
Do not let anyone talk you out of files. A CSV or JSON file is simple, portable, readable by humans, and needs nothing installed. For configuration, exports, and anything you write once and read whole, a file is the better answer and reaching for a database is overkill.
Files fall down on three specific things:
- Searching. Finding matching records means reading the entire file and checking each one yourself, every time.
- Changing one record. There is no way to edit line 4,000 in place. You read the whole file, change the item, and write the whole file back.
- Two things writing at once. Two processes writing the same file will interleave and corrupt it. Nothing stops them.
A database solves all three, because it was built for exactly these problems.
Tables, rows, and columns
A table is a named grid, and you already understand it because it is a spreadsheet. Each column has a name and a type; each row is one record. A database holds as many tables as you need.
The one genuinely new idea is that the structure is declared up front. You tell the database that books has a text title and an integer year, and from then on it enforces that. A CSV will happily let you put the word ‘banana’ in the year column.
books
┌────┬───────────────────┬──────┐
│ id │ title │ year │
├────┼───────────────────┼──────┤
│ 1 │ Dune │ 1965 │
│ 2 │ Neuromancer │ 1984 │
│ 3 │ Snow Crash │ 1992 │
└────┴───────────────────┴──────┘
SQL is a language you send, not a program you install
SQL is how you talk to the database: a short language for describing what you want rather than how to fetch it. You write SELECT title FROM books WHERE year > 1980 and the database works out how to do it.
SQL is not a separate application. It is text your Python program sends over a connection, exactly like the JSON you sent to an API in Unit 7. Python builds the string, the database runs it and hands back rows.
Choosing, in practice
Most decisions come down to size and whether you need to search. A few hundred records you always read in full is a file. Tens of thousands, or anything you query by condition, is a database.
Finish choose_storage. Return "database" when there are more than 1000 records or the data needs searching, and "file" otherwise.
def choose_storage(scenario):
return "file" # TODO: return "database" for big or searchable data
scenarios = [
{"name": "App settings", "records": 5, "needs_search": False},
{"name": "Customer orders", "records": 50000, "needs_search": True},
{"name": "One-off export", "records": 200, "needs_search": False},
{"name": "Site search index", "records": 800, "needs_search": True},
]
for scenario in scenarios:
print(f"{scenario['name']}: {choose_storage(scenario)}")
One if combining both conditions with or, returning "database", then return "file" underneath it.
def choose_storage(scenario):
if scenario["records"] > 1000 or scenario["needs_search"]:
return "database"
return "file"
scenarios = [
{"name": "App settings", "records": 5, "needs_search": False},
{"name": "Customer orders", "records": 50000, "needs_search": True},
{"name": "One-off export", "records": 200, "needs_search": False},
{"name": "Site search index", "records": 800, "needs_search": True},
]
for scenario in scenarios:
print(f"{scenario['name']}: {choose_storage(scenario)}")
App settings: file
Customer orders: database
One-off export: file
Site search index: database
Common mistake: Assuming every database needs a server installed
PostgreSQL and MySQL are the famous names, and both are servers, so that becomes the mental model.
SQLite is a file plus a library that already ships with Python. import sqlite3 is the entire installation, which makes it perfect for scripts and small apps.
Common mistake: Using a database for five configuration values
Databases feel like the more professional choice.
Five settings belong in a config file or environment variables. A database earns its place when you need to search, update single records, or handle more data than fits comfortably in memory.
Common mistake: Thinking SQL is a separate program to learn and run
It is a different language with different syntax, so it feels like a different tool.
SQL is text your Python code sends to the database, just like a request body sent to an API. You will write it inside ordinary Python strings.
Which of these is a genuinely good reason to move from a file to a database?
Searching, updating single records, and concurrent writes are what files do badly. Whole-file reads are exactly what files are good at.
What does SQLite need installed before you can use it from Python?
SQLite is a library plus a single file. import sqlite3 is the whole setup.
What is a primary key for?
A primary key is unique per row, which is what lets you point at a single record.
Mini exercise (easy)
Write storage_advice(records, needs_search, concurrent_writers) returning a (choice, reason) tuple. Any of: more than 1000 records, needing search, or more than one writer, means "database", each with its own reason. Otherwise "file".
Your turn. Fill in the code below and press Run to test it right here, nothing to install.
def storage_advice(records, needs_search, concurrent_writers):
return ("file", "small and simple") # TODO: check the three database reasons first
print(storage_advice(20, False, 1))
print(storage_advice(90000, False, 1))
print(storage_advice(50, True, 1))
print(storage_advice(50, False, 3))
Check each condition in its own if so you can return a specific reason for each, rather than combining them with or and losing which one applied.
def storage_advice(records, needs_search, concurrent_writers):
if records > 1000:
return ("database", "too many records to load in full")
if needs_search:
return ("database", "needs searching without reading everything")
if concurrent_writers > 1:
return ("database", "more than one writer at a time")
return ("file", "small and simple")
print(storage_advice(20, False, 1))
print(storage_advice(90000, False, 1))
print(storage_advice(50, True, 1))
print(storage_advice(50, False, 3))
('file', 'small and simple')
('database', 'too many records to load in full')
('database', 'needs searching without reading everything')
('database', 'more than one writer at a time')
assert storage_advice(10, False, 1)[0] == "file", "small, unsearched, single-writer data is a file"
assert storage_advice(10, True, 1)[0] == "database", "needing search means a database"
assert storage_advice(10, False, 4)[0] == "database", "several writers means a database"
print("✓ Looks good!")