Welcome back, @name! Darwin here with a crucial lesson about data storage.
So far you've worked with data temporarily - in variables, JSON files, scraped pages. But what if you have millions of species observations? What if you need to quickly search for all endangered species from Serengeti?
Then you need a database - a specialized system for storing and managing large datasets!
Safari Analogy: JSON files are like a biologist's notebook - works for 10 observations. A database is a large catalog library - organized drawers with index cards, indexes, and fast searching for millions of records!
A database is an organized collection of data + a management system (DBMS - Database Management System).
Why databases?
Structure: Tables with rows and columns (like an Excel spreadsheet)
Examples: PostgreSQL, MySQL, SQLite, Oracle, SQL Server
Characteristics:
1TABLE: species
2+----+-----------------+------------+----------+
3| id | scientific_name | population | habitat |
4+----+-----------------+------------+----------+
5| 1 | Panthera leo | 120 | savanna |
6| 2 | Gorilla gorilla | 230 | forest |
7+----+-----------------+------------+----------+Structure: Documents, key-value pairs, graphs, columns
Examples: MongoDB, Redis, Cassandra, Neo4j
Characteristics:
In this lesson: We focus on SQL (relational) - the foundation of most applications!
SQLite is the most popular SQL database in the world:
sqlite3 moduleUsage: Mobile apps, browsers, IoT devices, prototypes
SQL (Structured Query Language) is the universal language for communicating with relational databases.
1-- Species table
2CREATE TABLE species (
3 id INTEGER PRIMARY KEY AUTOINCREMENT,
4 scientific_name TEXT NOT NULL UNIQUE,
5 common_name TEXT NOT NULL,
6 population INTEGER DEFAULT 0,
7 habitat TEXT,
8 endangered BOOLEAN DEFAULT 0,
9 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
10);
11
12-- Observations table
13CREATE TABLE observations (
14 id INTEGER PRIMARY KEY AUTOINCREMENT,
15 species_id INTEGER NOT NULL,
16 observation_date DATE NOT NULL,
17 location TEXT NOT NULL,
18 count INTEGER DEFAULT 0,
19 notes TEXT,
20 FOREIGN KEY (species_id) REFERENCES species(id)
21);SQLite data types:
INTEGER - integer numberREAL - floating-point numberTEXT - textBLOB - binary dataNULL - no valueConstraints:
PRIMARY KEY - unique row identifierAUTOINCREMENT - automatic incrementNOT NULL - required fieldUNIQUE - unique valueDEFAULT - default valueFOREIGN KEY - relationship to another table1-- Add a single record
2INSERT INTO species (scientific_name, common_name, population, habitat, endangered)
3VALUES ('Panthera leo', 'Lion', 120, 'savanna', 1);
4
5-- Add multiple records
6INSERT INTO species (scientific_name, common_name, population, habitat, endangered)
7VALUES
8 ('Loxodonta africana', 'African Elephant', 450, 'savanna', 1),
9 ('Gorilla gorilla', 'Gorilla', 230, 'tropical forest', 1),
10 ('Python regius', 'Ball Python', 85, 'jungle', 0);
11
12-- Add an observation
13INSERT INTO observations (species_id, observation_date, location, count, notes)
14VALUES (1, '2024-01-15', 'Serengeti North', 12, 'Pride with 2 cubs');1-- All species
2SELECT * FROM species;
3
4-- Selected columns
5SELECT common_name, population FROM species;
6
7-- WHERE - filtering
8SELECT * FROM species WHERE endangered = 1;
9SELECT * FROM species WHERE population > 100;
10SELECT * FROM species WHERE habitat = 'savanna' AND endangered = 1;
11
12-- ORDER BY - sorting
13SELECT * FROM species ORDER BY population DESC; -- Descending
14SELECT * FROM species ORDER BY common_name ASC; -- Ascending
15
16-- LIMIT - limiting results
17SELECT * FROM species LIMIT 10;
18SELECT * FROM species LIMIT 10 OFFSET 20; -- Page 3 (20-30)
19
20-- LIKE - pattern matching
21SELECT * FROM species WHERE scientific_name LIKE 'Panthera%'; -- Starts with Panthera
22SELECT * FROM species WHERE habitat LIKE '%forest%'; -- Contains "forest"
23
24-- COUNT, SUM, AVG, MIN, MAX - aggregations
25SELECT COUNT(*) FROM species;
26SELECT AVG(population) FROM species;
27SELECT SUM(population) FROM species WHERE endangered = 1;
28SELECT MIN(population), MAX(population) FROM species;
29
30-- GROUP BY - grouping
31SELECT habitat, COUNT(*) as species_count
32FROM species
33GROUP BY habitat;
34
35SELECT habitat, AVG(population) as avg_population
36FROM species
37GROUP BY habitat
38HAVING AVG(population) > 100; -- HAVING = WHERE for groups1-- Update a single record
2UPDATE species
3SET population = 125
4WHERE id = 1;
5
6-- Update multiple records
7UPDATE species
8SET endangered = 1
9WHERE population < 50;
10
11-- Update multiple columns
12UPDATE species
13SET population = 130, habitat = 'savanna and steppe'
14WHERE scientific_name = 'Panthera leo';1-- Delete a single record
2DELETE FROM species WHERE id = 10;
3
4-- Delete multiple records
5DELETE FROM species WHERE population = 0;
6
7-- WARNING: Delete everything (no WHERE!)
8DELETE FROM species; -- Deletes ALL records!1-- INNER JOIN - only matching records
2SELECT
3 species.common_name,
4 observations.observation_date,
5 observations.location,
6 observations.count
7FROM observations
8INNER JOIN species ON observations.species_id = species.id;
9
10-- LEFT JOIN - all from left + matching from right
11SELECT
12 species.common_name,
13 COUNT(observations.id) as observation_count
14FROM species
15LEFT JOIN observations ON species.id = observations.species_id
16GROUP BY species.id, species.common_name;
17
18-- Table aliases (s, o)
19SELECT
20 s.common_name,
21 o.location,
22 o.count
23FROM observations o
24INNER JOIN species s ON o.species_id = s.id
25WHERE s.endangered = 1;Python has a built-in
sqlite3 module for working with SQLite.1import sqlite3
2
3# 1. Connect to database (creates file if it doesn't exist)
4conn = sqlite3.connect("safari.db")
5
6# 2. Create cursor (object for executing queries)
7cursor = conn.cursor()
8
9# 3. Execute SQL query
10cursor.execute("""
11 CREATE TABLE IF NOT EXISTS species (
12 id INTEGER PRIMARY KEY AUTOINCREMENT,
13 scientific_name TEXT NOT NULL UNIQUE,
14 common_name TEXT NOT NULL,
15 population INTEGER DEFAULT 0,
16 endangered BOOLEAN DEFAULT 0
17 )
18""")
19
20# 4. Commit changes (IMPORTANT!)
21conn.commit()
22
23# 5. Close connection
24conn.close()1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4cursor = conn.cursor()
5
6# Method 1: Query with values (DANGEROUS - SQL injection!)
7# DON'T DO THIS:
8# name = "Lion'; DROP TABLE species; --" # SQL injection!
9# cursor.execute(f"INSERT INTO species (common_name) VALUES ('{name}')")
10
11# Method 2: Parameterized queries (SAFE)
12cursor.execute("""
13 INSERT INTO species (scientific_name, common_name, population, endangered)
14 VALUES (?, ?, ?, ?)
15""", ("Panthera leo", "Lion", 120, 1))
16
17# Multiple records at once
18species_data = [
19 ("Loxodonta africana", "African Elephant", 450, 1),
20 ("Gorilla gorilla", "Gorilla", 230, 1),
21 ("Python regius", "Ball Python", 85, 0)
22]
23
24cursor.executemany("""
25 INSERT INTO species (scientific_name, common_name, population, endangered)
26 VALUES (?, ?, ?, ?)
27""", species_data)
28
29conn.commit()
30
31# Get the ID of the last added record
32print(f"Added species ID: {cursor.lastrowid}")
33
34conn.close()1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4cursor = conn.cursor()
5
6# fetchall() - all results as a list of tuples
7cursor.execute("SELECT * FROM species")
8all_species = cursor.fetchall()
9
10for species in all_species:
11 print(species) # (1, 'Panthera leo', 'Lion', 120, 1)
12
13# fetchone() - one result
14cursor.execute("SELECT * FROM species WHERE id = ?", (1,))
15lion = cursor.fetchone()
16print(lion) # (1, 'Panthera leo', 'Lion', 120, 1)
17
18# fetchmany(n) - n results
19cursor.execute("SELECT * FROM species")
20first_five = cursor.fetchmany(5)
21
22# Row factory - results as dictionaries
23conn.row_factory = sqlite3.Row
24cursor = conn.cursor()
25
26cursor.execute("SELECT * FROM species")
27for row in cursor.fetchall():
28 print(row["common_name"], row["population"])
29
30conn.close()1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4cursor = conn.cursor()
5
6# UPDATE
7cursor.execute("""
8 UPDATE species
9 SET population = ?
10 WHERE id = ?
11""", (125, 1))
12
13print(f"Updated {cursor.rowcount} records")
14
15# DELETE
16cursor.execute("DELETE FROM species WHERE population < ?", (10,))
17print(f"Deleted {cursor.rowcount} records")
18
19conn.commit()
20conn.close()1import sqlite3
2
3# Automatic commit and close
4with sqlite3.connect("safari.db") as conn:
5 cursor = conn.cursor()
6
7 cursor.execute("""
8 INSERT INTO species (scientific_name, common_name, population)
9 VALUES (?, ?, ?)
10 """, ("Acinonyx jubatus", "Cheetah", 7100))
11
12 # commit() automatically on exiting the with block
13# close() automatically1import sqlite3
2from typing import List, Dict, Optional
3from datetime import datetime
4
5class SafariDatabase:
6 """Complete Safari data management system"""
7
8 def __init__(self, db_path: str = "safari.db"):
9 self.db_path = db_path
10 self.init_database()
11
12 def get_connection(self):
13 """Create database connection"""
14 conn = sqlite3.connect(self.db_path)
15 conn.row_factory = sqlite3.Row # Results as dictionaries
16 return conn
17
18 def init_database(self):
19 """Initialize database schema"""
20 with self.get_connection() as conn:
21 cursor = conn.cursor()
22
23 # Species table
24 cursor.execute("""
25 CREATE TABLE IF NOT EXISTS species (
26 id INTEGER PRIMARY KEY AUTOINCREMENT,
27 scientific_name TEXT NOT NULL UNIQUE,
28 common_name TEXT NOT NULL,
29 population INTEGER DEFAULT 0,
30 habitat TEXT,
31 endangered BOOLEAN DEFAULT 0,
32 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
33 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
34 )
35 """)
36
37 # Observations table
38 cursor.execute("""
39 CREATE TABLE IF NOT EXISTS observations (
40 id INTEGER PRIMARY KEY AUTOINCREMENT,
41 species_id INTEGER NOT NULL,
42 observation_date DATE NOT NULL,
43 location TEXT NOT NULL,
44 count INTEGER DEFAULT 0,
45 notes TEXT,
46 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
47 FOREIGN KEY (species_id) REFERENCES species(id) ON DELETE CASCADE
48 )
49 """)
50
51 # Indexes for faster searching
52 cursor.execute("""
53 CREATE INDEX IF NOT EXISTS idx_species_endangered
54 ON species(endangered)
55 """)
56
57 cursor.execute("""
58 CREATE INDEX IF NOT EXISTS idx_observations_species
59 ON observations(species_id)
60 """)
61
62 conn.commit()
63
64 # === SPECIES CRUD ===
65
66 def create_species(self, scientific_name: str, common_name: str,
67 population: int = 0, habitat: str = "",
68 endangered: bool = False) -> int:
69 """Add a new species"""
70 with self.get_connection() as conn:
71 cursor = conn.cursor()
72 cursor.execute("""
73 INSERT INTO species (scientific_name, common_name, population, habitat, endangered)
74 VALUES (?, ?, ?, ?, ?)
75 """, (scientific_name, common_name, population, habitat, int(endangered)))
76
77 conn.commit()
78 return cursor.lastrowid
79
80 def get_species(self, species_id: int) -> Optional[Dict]:
81 """Get species by ID"""
82 with self.get_connection() as conn:
83 cursor = conn.cursor()
84 cursor.execute("SELECT * FROM species WHERE id = ?", (species_id,))
85 row = cursor.fetchone()
86 return dict(row) if row else None
87
88 def list_species(self, endangered: Optional[bool] = None,
89 habitat: Optional[str] = None,
90 min_population: int = 0) -> List[Dict]:
91 """List species with filters"""
92 with self.get_connection() as conn:
93 cursor = conn.cursor()
94
95 query = "SELECT * FROM species WHERE population >= ?"
96 params = [min_population]
97
98 if endangered is not None:
99 query += " AND endangered = ?"
100 params.append(int(endangered))
101
102 if habitat:
103 query += " AND habitat = ?"
104 params.append(habitat)
105
106 query += " ORDER BY common_name"
107
108 cursor.execute(query, params)
109 return [dict(row) for row in cursor.fetchall()]
110
111 def update_species(self, species_id: int, **kwargs) -> bool:
112 """Update a species"""
113 if not kwargs:
114 return False
115
116 # Dynamic UPDATE building
117 fields = ", ".join([f"{key} = ?" for key in kwargs.keys()])
118 values = list(kwargs.values())
119 values.append(species_id)
120
121 with self.get_connection() as conn:
122 cursor = conn.cursor()
123 cursor.execute(f"""
124 UPDATE species
125 SET {fields}, updated_at = CURRENT_TIMESTAMP
126 WHERE id = ?
127 """, values)
128
129 conn.commit()
130 return cursor.rowcount > 0
131
132 def delete_species(self, species_id: int) -> bool:
133 """Delete a species"""
134 with self.get_connection() as conn:
135 cursor = conn.cursor()
136 cursor.execute("DELETE FROM species WHERE id = ?", (species_id,))
137 conn.commit()
138 return cursor.rowcount > 0
139
140 # === OBSERVATIONS ===
141
142 def create_observation(self, species_id: int, observation_date: str,
143 location: str, count: int, notes: str = "") -> int:
144 """Add an observation"""
145 with self.get_connection() as conn:
146 cursor = conn.cursor()
147 cursor.execute("""
148 INSERT INTO observations (species_id, observation_date, location, count, notes)
149 VALUES (?, ?, ?, ?, ?)
150 """, (species_id, observation_date, location, count, notes))
151
152 conn.commit()
153 return cursor.lastrowid
154
155 def get_observations_for_species(self, species_id: int) -> List[Dict]:
156 """Get observations for a species"""
157 with self.get_connection() as conn:
158 cursor = conn.cursor()
159 cursor.execute("""
160 SELECT
161 o.*,
162 s.common_name as species_name
163 FROM observations o
164 JOIN species s ON o.species_id = s.id
165 WHERE o.species_id = ?
166 ORDER BY o.observation_date DESC
167 """, (species_id,))
168
169 return [dict(row) for row in cursor.fetchall()]
170
171 # === STATISTICS ===
172
173 def get_statistics(self) -> Dict:
174 """Get database statistics"""
175 with self.get_connection() as conn:
176 cursor = conn.cursor()
177
178 # Total species count
179 cursor.execute("SELECT COUNT(*) as count FROM species")
180 total_species = cursor.fetchone()["count"]
181
182 # Endangered species
183 cursor.execute("SELECT COUNT(*) as count FROM species WHERE endangered = 1")
184 endangered_count = cursor.fetchone()["count"]
185
186 # Total population
187 cursor.execute("SELECT SUM(population) as total FROM species")
188 total_population = cursor.fetchone()["total"] or 0
189
190 # Species by habitat
191 cursor.execute("""
192 SELECT habitat, COUNT(*) as count
193 FROM species
194 GROUP BY habitat
195 ORDER BY count DESC
196 """)
197 by_habitat = [dict(row) for row in cursor.fetchall()]
198
199 return {
200 "total_species": total_species,
201 "endangered_count": endangered_count,
202 "total_population": total_population,
203 "by_habitat": by_habitat
204 }
205
206
207# === DEMONSTRATION ===
208
209print("=== SAFARI DATABASE SYSTEM ===\n")
210
211db = SafariDatabase("safari_demo.db")
212
213# 1. Add species
214print("1. Adding species...")
215lion_id = db.create_species("Panthera leo", "Lion", 120, "savanna", True)
216elephant_id = db.create_species("Loxodonta africana", "African Elephant", 450, "savanna", True)
217gorilla_id = db.create_species("Gorilla gorilla", "Gorilla", 230, "tropical forest", True)
218python_id = db.create_species("Python regius", "Ball Python", 85, "jungle", False)
219
220print(f" Added {4} species")
221
222# 2. Fetch species
223print("\n2. Fetching species...")
224lion = db.get_species(lion_id)
225print(f" {lion['common_name']} ({lion['scientific_name']})")
226print(f" Population: {lion['population']}, Endangered: {'Yes' if lion['endangered'] else 'No'}")
227
228# 3. List species with filter
229print("\n3. List of endangered species...")
230endangered = db.list_species(endangered=True)
231for species in endangered:
232 print(f" - {species['common_name']}: {species['population']} individuals")
233
234# 4. Update
235print("\n4. Updating lion population...")
236db.update_species(lion_id, population=125)
237lion = db.get_species(lion_id)
238print(f" New population: {lion['population']}")
239
240# 5. Add observations
241print("\n5. Adding observations...")
242db.create_observation(lion_id, "2024-01-15", "Serengeti North", 12, "Pride with 2 cubs")
243db.create_observation(lion_id, "2024-01-20", "Masai Mara", 8, "Male coalition")
244db.create_observation(elephant_id, "2024-01-16", "Amboseli", 35, "Large herd")
245
246print(" Added 3 observations")
247
248# 6. Fetch observations
249print("\n6. Lion observations...")
250lion_obs = db.get_observations_for_species(lion_id)
251for obs in lion_obs:
252 print(f" - {obs['observation_date']}: {obs['count']}x in {obs['location']}")
253
254# 7. Statistics
255print("\n7. Database statistics...")
256stats = db.get_statistics()
257print(f" Total species: {stats['total_species']}")
258print(f" Endangered: {stats['endangered_count']}")
259print(f" Total population: {stats['total_population']}")
260print(" By habitat:")
261for habitat_stat in stats['by_habitat']:
262 print(f" - {habitat_stat['habitat']}: {habitat_stat['count']} species")
263
264print("\nDemonstration complete")1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4
5try:
6 cursor = conn.cursor()
7
8 # Begin transaction (default)
9 cursor.execute("INSERT INTO species (...) VALUES (...)")
10 cursor.execute("UPDATE observations SET ...")
11
12 # Commit transaction
13 conn.commit()
14 print("Transaction committed")
15
16except Exception as e:
17 # Roll back transaction on error
18 conn.rollback()
19 print(f"Transaction rolled back: {e}")
20
21finally:
22 conn.close()In this lesson you learned:
Before moving on:
Safari Analogy: An SQL database is a large catalog library with millions of species cards - fast searching, relationships, data integrity. JSON is a notebook - great for 10 entries, insufficient for millions!
In the next lesson, Darwin will show you SQLAlchemy ORM - how to work with databases using Python classes instead of raw SQL!