Chapter 23 Exercises: Database Basics
These exercises use the Acme Corp inventory database created by sqlite_demo.py. Run that script first to generate acme_inventory.db, then connect to it for the exercises below.
Each tier builds on the previous. Work through them in order unless your instructor specifies otherwise.
Tier 1 — Foundations (Connect, Create, Insert)
Exercise 1.1 — First Connection
Write a Python script that:
1. Connects to acme_inventory.db using sqlite3
2. Prints the number of rows in the products table
3. Prints the number of rows in the customers table
4. Closes the connection
Expected output format:
Products: 18
Customers: 8
Exercise 1.2 — Create a Suppliers Table
Add a suppliers table to acme_inventory.db with the following columns:
- id — integer, primary key, autoincrement
- name — text, not null, unique
- contact_email — text, not null
- phone — text
- city — text
- state — text
Then insert three rows of sample data for fictional suppliers. Verify by printing all rows with SELECT * FROM suppliers.
Exercise 1.3 — Safe Insert Practice
Write a function add_product(conn, sku, name, category, unit_price, quantity) that:
1. Uses a parameterized INSERT to add a product
2. Returns the new product's auto-generated ID (cursor.lastrowid)
3. Raises a descriptive error if the SKU already exists
4. Includes a docstring explaining the parameters
Test it by inserting one new product and printing the returned ID.
Exercise 1.4 — Row Factory Exploration
Connect to the database without setting row_factory. Then connect again with conn.row_factory = sqlite3.Row.
Run the same query on both connections:
SELECT id, sku, name FROM products LIMIT 3
Print the results from each connection. Observe and comment in your script on the difference between accessing columns by index vs. by name.
Exercise 1.5 — fetchone, fetchall, fetchmany
Write a script that demonstrates all three fetch methods on the products table:
1. Use fetchone() to get the most expensive product
2. Use fetchall() to get all products in the "Supplies" category
3. Use fetchmany(3) to get the first 3 products ordered alphabetically by name
Print a label before each result so the output is clear.
Tier 2 — Core SQL
Exercise 2.1 — WHERE and ORDER BY
Write SQL queries (executed through Python) to answer each question. Print the results using pd.read_sql().
a. Which products have a unit price greater than $50? b. Which products are in the "Electronics" category, sorted by price descending? c. Which products have fewer than 20 units in stock? d. Which products have a name that starts with "Widget"? (Use LIKE)
Exercise 2.2 — Aggregate Functions
Write queries to answer:
a. What is the average unit price across all products? b. What is the total number of units across all products (sum of quantity)? c. What is the most expensive product? Show SKU, name, and price. d. How many products are there in each category? (Use GROUP BY) e. What is the total inventory value (unit_price × quantity) per category, sorted highest to lowest?
Exercise 2.3 — HAVING Clause
Write a query that shows only product categories where: - There are at least 3 products in the category, AND - The average unit price is above $20
Show category, product count, and average price.
Exercise 2.4 — INNER JOIN
Write a query that returns: - Customer name - Order ID - Order date - Order status - Order total
For all orders placed in the last 30 days. Sort by order date descending.
Hint: Use strftime('%Y-%m-%d', order_date) >= date('now', '-30 days') in SQLite.
Exercise 2.5 — LEFT JOIN and Missing Data
Write a query to find all customers who have not placed any orders. Show customer name, email, city, and state.
Use a LEFT JOIN with a WHERE orders.id IS NULL condition.
Exercise 2.6 — Three-Table Join
Write a query that returns one row per order line, showing: - Customer name - Order date - Product name - Quantity ordered - Unit price at time of order - Line total (quantity × unit_price)
Sort by customer name, then order date.
Tier 3 — Data Modification and Transactions
Exercise 3.1 — UPDATE with Verification
Write a function update_price(conn, sku, new_price) that:
1. Updates the unit price for the given SKU
2. Verifies the update was successful using cursor.rowcount
3. Returns True if the update succeeded, False if the SKU was not found
4. Uses a parameterized query
Test it with a valid SKU and an invalid SKU ("SKU-NOTEXIST").
Exercise 3.2 — Transaction Rollback
Simulate a transaction that should fail. Write code that: 1. Begins a transaction 2. Updates the quantity for two products (reduce by 5 each) 3. Attempts to insert an order line referencing a product_id that does not exist 4. Handles the exception by rolling back the transaction 5. Verifies the product quantities were NOT changed (the rollback worked)
Exercise 3.3 — Bulk Insert with executemany
Write a function that reads a CSV file with columns sku, name, category, unit_price, quantity and inserts all rows into the products table using executemany().
Create a small test CSV file (5 rows) and run the function. Use INSERT OR IGNORE so duplicate SKUs are skipped. Print how many rows were inserted vs. how many were skipped.
Exercise 3.4 — DELETE with Confirmation
Write a function delete_product(conn, sku) that:
1. Checks if the product has any associated order lines before deleting
2. If it does, raises an error with a message explaining why it cannot be deleted
3. If it does not, deletes the product and returns True
4. Prompts for confirmation before deleting (use input())
Tier 4 — SQLAlchemy ORM
Exercise 4.1 — Define a Model
Using SQLAlchemy, define a Supplier model that maps to a suppliers table with:
- id (Integer, primary key, autoincrement)
- name (String, not null, unique)
- contact_email (String, not null)
- phone (String, optional)
- city (String, optional)
- products (relationship to Product — one supplier has many products)
You will need to add a supplier_id foreign key column to the Product model.
Create the tables, insert two suppliers, and query all suppliers using the ORM.
Exercise 4.2 — ORM CRUD
Using the SQLAlchemy models from sqlalchemy_demo.py:
a. Add a new product using session.add() and commit it.
b. Load that product by its SKU using session.query().filter().first().
c. Update its quantity and unit_price. Commit.
d. Verify the changes by querying again.
e. Delete the product using session.delete(). Commit.
f. Verify it is gone.
Exercise 4.3 — ORM Relationships
Using the Order and Customer models:
1. Create a new customer
2. Create two orders for that customer using the customer relationship attribute (not customer_id)
3. For each order, add at least two OrderLine objects
4. Commit everything
5. Load the customer back and navigate to their orders and line items using Python attribute access (customer.orders, order.order_lines, line.product)
Exercise 4.4 — ORM vs Raw SQL Comparison
Write the same query two ways:
Query: Total revenue per customer (sum of order totals for completed orders), top 5.
Version A: Using SQLAlchemy ORM query with session.query(), func.sum(), and group_by().
Version B: Using pd.read_sql() with a raw SQL string.
Compare the two approaches: which is more readable? Which was easier to write? Add a comment block in your script sharing your perspective.
Tier 5 — Real-World Challenges
Exercise 5.1 — Data Integrity Audit
Write a comprehensive data integrity check function that queries acme_inventory.db and reports:
1. Products with negative quantity
2. Orders with a total_amount of 0 but that have order lines
3. Orders where the stored total_amount does not match the sum of the line items (within $0.01)
4. Customers with no contact information (no email AND no phone)
Output a report as a pandas DataFrame with columns: issue_type, affected_id, description, detail.
Exercise 5.2 — Inventory Turnover Report
Write a query and Python function that calculates inventory turnover for each product category:
Inventory Turnover = Cost of Goods Sold / Average Inventory Value
For simplicity, use: - "Cost of Goods Sold" = sum of (quantity × unit_price) across all completed order lines - "Average Inventory Value" = current (quantity × unit_price)
Products with higher turnover are selling quickly relative to inventory held.
Export the result to both a printed table and a CSV file named inventory_turnover.csv.
Exercise 5.3 — Database Migration Script
Write a script that migrates the Acme Corp SQLite database to a new schema version. The migration should:
- Add a
discount_percentcolumn toproducts(default 0.0) - Add a
loyalty_tiercolumn tocustomers(TEXT, default 'standard') - Calculate loyalty tier based on total order value: - 'standard' — less than $500 total - 'silver' — $500 to $2,000 - 'gold' — more than $2,000
- Update all existing customers with their calculated tier
Wrap the entire migration in a transaction. Add a schema_version table that records each migration that has been applied, with a timestamp.
Exercise 5.4 — Python CLI for the Database
Build a command-line interface for the Acme Corp inventory database using Python's argparse module:
python inventory_cli.py list-products [--category CATEGORY] [--low-stock]
python inventory_cli.py add-product --sku SKU --name NAME --category CAT --price PRICE --qty QTY
python inventory_cli.py search SEARCH_TERM
python inventory_cli.py report --type {inventory|customers|revenue}
Requirements:
- All database operations must use parameterized queries
- list-products --low-stock shows only products at or below reorder point
- search TERM searches product names and SKUs (use LIKE)
- report --type inventory exports a CSV summary
Exercise 5.5 — PostgreSQL Connection Swap (Conceptual)
This exercise does not require a running PostgreSQL server. Instead:
- Take your
sqlite_demo.pyscript and create a copy calledpostgres_ready.py - Refactor it to use SQLAlchemy instead of
sqlite3directly - Use
python-dotenvto load the connection string from a.envfile - Add a
--dbcommand-line argument that acceptssqliteorpostgres - When
--db sqliteis passed, usesqlite:///acme_inventory.db - When
--db postgresis passed, build the connection string from.envvariables
Write a short comment block explaining what additional steps (driver installation, server setup, credential management) would be needed to actually connect to PostgreSQL.
Bonus Challenge: Build Something Real
Choose one of the following and build it from scratch:
Option A — Personal Finance Tracker
Tables: accounts, transactions, categories, budgets. Write queries for: monthly spending by category, accounts over budget, net worth calculation.
Option B — Event Management System
Tables: events, attendees, registrations, venues. Write queries for: upcoming events, sold-out events, attendee lists, revenue per event.
Option C — Job Application Tracker
Tables: companies, applications, contacts, interviews, notes. Write queries for: applications by status, response rate by source, interview success rate.
For your chosen option, include: schema design document (comments in code), all CRUD operations, at least 5 analytical queries, and a data export function.