Why analysts learn SQL even with Excel
Everything you have done so far used bisi-provisions-sales-clean.csv, a single flat table with branch, item, category and amount_ngn sitting conveniently in every row.
Real business data does not look like that. Open bisi-provisions.db and the same business looks like this:
| Table | Rows | What's in it |
|---|---|---|
sales | 2,010 | One row per line sold — but with IDs, not names |
products | 20 | Product names, categories, cost and list prices |
customers | 19 | Customer names, home branch, first purchase |
branches | 3 | Branch names and opening dates |
And the first row of sales reads:
sale_id sale_date receipt_no branch_id customer_id product_id qty unit_price_ngn payment_type cashier
1 2026-01-02 R10001 B1 C00 P01 1 86500 POS Amaka
No branch name. No item name. No category. And no amount column at all.
Somebody flattened that into the CSV you've been using. This module teaches you to do it yourself, which is what an analyst is actually hired to do.
Words in this lesson
| Word | What it means, in plain English |
|---|---|
| Database | Data stored in linked tables, built to be queried. |
| Query | A question written in SQL. |
| SQL | Structured Query Language, the language for asking databases questions. |
| Table | A set of rows and columns, like a sheet. |
| Primary key | The column that uniquely identifies a row, like product_id. |
| Foreign key | A column pointing at another table's key, like sales.product_id. |
| Normalised | Each fact stored once, in one place. |
Four reasons analysts learn SQL
1. The data lives there. Almost every till system, bank portal, hospital record and web app stores its data in a database. The CSV you get is an export — someone's decision about what to include. Learn SQL and you stop waiting for exports, and stop accepting someone else's choice of columns.
2. Size. A spreadsheet struggles past a few hundred thousand rows (lesson 1.5). Databases handle millions without noticing. Bisi's half-year is 2,010 rows; five years across thirty branches would be far past comfortable in Sheets.
3. Repeatability. A query is text. Save it, re-run it next month, and it does exactly the same thing to the new data. No dragging, no ranges to update, no chance of a formula that quietly stopped at row 938. This is the change-log discipline from lesson 1.2 made automatic.
4. It is the most-asked-for skill on analyst job adverts. Not because it's hard, but because it's the boundary between someone who receives data and someone who can go and get it.
Why the database looks so inconvenient
The CSV repeats "Rice (50kg bag)" on all 42 rows where it sold. The database stores it once, in products, and refers to it as P01.
That's called normalising, and it has three practical consequences:
| Consequence | Why it matters |
|---|---|
| Each fact is stored once | Correct a misspelled product name in one place and every sale is fixed |
| Bad spellings are almost impossible | You can't type "ikeja" into branch_id; it must match a real branch |
| You must join to see names | Which is why lesson 6.4 exists |
Look back at lesson 2.4, where you spent a whole lesson merging nine spellings of three branch names. That entire problem cannot happen in this database. The branch is B1, B2 or B3, and the name lives in one row of one table.
That is what people mean when they say a database enforces quality that a spreadsheet cannot.
The missing amount column
sales has qty and unit_price_ngn, but no amount. That is deliberate, and it is the same principle as lesson 2.2.
An amount is derived: quantity times unit price. Store it as well and you have two versions of one fact, which can disagree — exactly the disagreement you found on rows 58 and 428 of the raw export. Store only what you can't calculate, and calculate the rest on the way out:
SELECT qty * unit_price_ngn AS amount FROM sales;
But why is there a price in the sales table at all?
If products has list_price_ngn, why does each sale carry its own unit_price_ngn?
Because the list price is what a product costs today, and a sale must record what it cost on the day it sold. Prices change. If sales didn't store their own price, every historical figure would silently rewrite itself the next time Bisi put her prices up.
Hold on to that. In lesson 6.4 you'll find that four of Bisi's products did change price during this period, and that the products table hasn't been updated — a fact with real consequences for anyone analysing her second quarter.
Getting the file open
You already downloaded bisi-provisions.db in Module 1. If not, it's on the first lesson of this course with the other datasets.
The friendly option: DB Browser for SQLite. According to its website, DB Browser for SQLite (DB4S) is an open source visual tool for people who want to create, search and edit SQLite database files, giving a spreadsheet-like interface to the database in addition to a full SQL query facility. It works on Windows, macOS and most versions of Linux, and downloads are at sqlitebrowser.org/dl/.
Open the file, click Browse Data to look around, and Execute SQL to run queries.
The official option: the sqlite3 command line. SQLite's documentation describes sqlite3 as a command-line program that lets you run SQL statements interactively against a database, and says it is the officially supported way of accessing SQLite files interactively. You start it by typing sqlite3 followed by the database filename:
sqlite3 bisi-provisions.db
SQLite's docs also note that if the named file doesn't exist, a new empty database is created automatically — so check your spelling, or you'll get an empty database and a confusing set of zero results.
Start with DB Browser. The visual table list makes the structure obvious in a way the command line doesn't.
Your first three queries
Type these. They take a minute and they orient you completely.
SELECT * FROM branches;
| branch_id | branch_name | opened_on |
|---|---|---|
| B1 | Yaba | 2021-03-01 |
| B2 | Ikeja | 2023-06-15 |
| B3 | Surulere | 2025-11-01 |
SELECT COUNT(*) FROM sales;
→ 2010
SELECT COUNT(*) AS lines, SUM(qty * unit_price_ngn) AS revenue
FROM sales
WHERE sale_date < '2026-04-01';
→ 938 lines, 11026300
That is the same 938 rows and the same ₦11,026,300 you proved in lesson 2.5. Two completely different tools, one answer. If you ever get a different number from a database than from a spreadsheet, one of them is wrong and it is your job to find out which.
Already a question worth asking
Look at that branches table again. Surulere's opened_on says 2025-11-01.
But every module since lesson 3.4 has told you Surulere's first sale was 2 February 2026. Three months apart.
SELECT b.branch_name, b.opened_on, MIN(s.sale_date) AS first_sale
FROM branches b
JOIN sales s ON s.branch_id = b.branch_id
GROUP BY b.branch_name, b.opened_on;
| branch_name | opened_on | first_sale |
|---|---|---|
| Yaba | 2021-03-01 | 2026-01-02 |
| Ikeja | 2023-06-15 | 2026-01-02 |
| Surulere | 2025-11-01 | 2026-02-02 |
Yaba and Ikeja are easy: they opened years before this data starts, so their first sale is simply the first day in the file. Surulere is not: the branch record claims November, and the sales say February.
Which is right? The data cannot tell you. Maybe the shop was fitted out in November and didn't trade until February. Maybe someone typed the wrong date into the branch record. Maybe November's sales were never migrated into this system.
The professional move is not to pick one. It is to write it down and ask:
"The branches table gives Surulere an opening date of 2025-11-01, but its first recorded sale is 2026-02-02. I've used February for all comparisons because that's when revenue starts, but this needs confirming — if sales from November to January exist somewhere, every Surulere figure in this report is understated."
You will meet this constantly. Reference tables and transaction tables disagree, because one is maintained by a person and the other is generated by a machine. Noticing is the job.
What SQL will and won't do for you
| SQL is good at | SQL will not |
|---|---|
| Filtering millions of rows fast | Tell you which question to ask |
| Combining tables | Decide whether a comparison is fair |
| Repeating exactly, forever | Notice that Surulere opened late |
| Aggregating and grouping | Format a chart for a shop owner |
Everything in Modules 1 to 5 still applies. SQL is a faster way to get numbers; it is not a substitute for knowing whether the numbers mean anything.
Common mistakes
| Mistake | Better |
|---|---|
| Treating the CSV export as "the data" | It is someone's choice of columns; the database is the source |
| Expecting names in the sales table | It stores IDs; join to get names (lesson 6.4) |
| Looking for an amount column | Amounts are derived: qty * unit_price_ngn |
Mistyping the filename in sqlite3 | It creates an empty database instead of an error |
| Assuming the database is automatically correct | Surulere's opening date disagrees with its sales |
| Thinking SQL replaces analytical judgement | It replaces the typing, not the thinking |
The short version
- Bisi's database has four tables:
sales(2,010),products(20),customers(19),branches(3). salesholds IDs, not names, and no amount column — amounts areqty * unit_price_ngn.- Normalising stores each fact once, which makes lesson 2.4's nine branch spellings impossible.
- Each sale stores its own price, because list prices change and history must not rewrite itself.
- Analysts learn SQL for access, size, repeatability, and because it's the most requested skill.
- Tools: DB Browser for SQLite (visual, spreadsheet-like, plus full SQL) or the
sqlite3CLI (officially supported). - The database gives the same 938 lines / ₦11,026,300 you proved in Module 2.
- Already found:
branches.opened_onsays Surulere opened 2025-11-01, but its first sale is 2026-02-02. Flag it and ask.
Try it yourself
- Open
bisi-provisions.dbin DB Browser for SQLite and look at all four tables in Browse Data. - Run
SELECT * FROM branches;andSELECT * FROM products;. Write down which column is each table's primary key. - Run the Q1 count and sum. Confirm 938 and 11026300 against your Module 2 figure.
- Find the
customersrow with idC00. Write one sentence on what it represents and why it exists. - Run the opening-date query. Write the note you'd send Bisi about Surulere, in two sentences.
- Count the rows in
salesfor the whole file, and explain in one sentence why it's 2,010 and not 938. - Look at
productsand find the row forP01. Explain why the sales table stores a price as well.
You'll know you've got it when you can describe, without looking, what each of the four tables holds and how they connect — and when you'd rather be given the database than the spreadsheet.
More free previews
- ▶What analysts actually spend their time on15 min
- ▶The five things that break a spreadsheet20 min
- ▶SUM, COUNT and AVERAGE without embarrassing yourself20 min
- ▶Averages that mislead: mean, median and mode20 min
- ▶Your first pivot table20 min
- ▶Choose the right chart for the question20 min
- ▶Write findings a busy manager will act on25 min
- ▶Choose a dataset and a question worth answering20 min