SQL for Data Analysts: Online Store
Write SQL queries to analyze an e-commerce database like a real data analyst.
Introduction
30 Second Summary
Every business has questions about its data. Which products sell best? Who are the top customers? SQL is the language that answers these questions, and it is the single most tested skill in data analyst interviews.
In this project, you will build an e-commerce database from scratch, load it with realistic data, and write SQL queries to answer business questions for a fictional online store called ShopSmart.
What You'll Build
A SQLite database with related tables for customers, products, and orders, plus a collection of SQL queries that answer real business questions.
By the end of this project, you'll have:
- A working e-commerce database with 4 related tables (customers, products, orders, and order_items) built inside DB Browser for SQLite.
- A collection of SQL queries that answer business questions like "Who are our top customers?" and "What categories drive the most revenue?".
- Hands-on experience with the exact SQL patterns tested in data analyst interviews: JOINs, GROUP BY, aggregate functions, and filtering.
- Secret Mission: Write advanced queries using subqueries and HAVING to find VIP customers and underperforming products.
Are there any prerequisites?
None. This project includes full setup instructions for every tool you need. No prior SQL or database experience is required.
Before We Start
Before We Start...
Install DB Browser for SQLite
Before you can write any SQL, you need a tool that lets you create databases and run queries.
In this step, you will install DB Browser for SQLite, a free visual tool that gives you a built-in SQL editor with immediate results. You will also create the empty database file for our ShopSmart online store.
In this step, get ready to:
- Install Homebrew (the macOS package manager).
- Install DB Browser for SQLite.
- Create an empty database file called shopmart.db.
Install Homebrew
Homebrew is a package manager for macOS that lets you install software from the command line. You will use it to install DB Browser for SQLite in the next substep.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Check if Homebrew is already installed by running this command:
brew --version
✔️ I see a version number
Homebrew is already installed and ready to go. Skip ahead to the next substep.
ⓧ Command not found
Homebrew is not installed yet. Install it by running this command in your Terminal:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
What does this command do?
This downloads and runs the official Homebrew installer script from GitHub. The script will explain what it plans to do and ask for your confirmation before making any changes.
- When prompted, enter your Mac password and press Enter.
- Press Enter again to confirm the installation when asked.
- Wait for the installation to finish. This may take a few minutes.
Follow the post-install instructions
After installing, Homebrew may print "Next steps" in your Terminal telling you to run additional commands to add Homebrew to your PATH. Copy and run those commands exactly as shown.
- Confirm Homebrew installed successfully by running:
brew --version
You should see a version number like Homebrew 5.0.0 or similar.
Still seeing command not found?
Make sure you ran any "Next steps" commands that Homebrew printed after installation. Try closing your Terminal completely and opening a new one, then run brew --version again.
help me troubleshoot Homebrew not being found after installation on macOS
Install DB Browser for SQLite
DB Browser for SQLite is a free, open-source visual tool for creating and querying SQLite databases. It has a built-in SQL editor that shows query results instantly.
- Install DB Browser for SQLite by running this command:
brew install db-browser-for-sqlite
What does this command do?
This tells Homebrew to download and install the DB Browser for SQLite application. Homebrew handles all the setup so you can launch it like any other app on your Mac.
- Wait for the installation to complete. You will see progress messages in your Terminal.
- Launch DB Browser for SQLite by pressing Cmd+Space, typing DB Browser, and pressing Enter.
You should see the DB Browser for SQLite application open with a blank workspace.
DB Browser not appearing in search?
It may take a moment for macOS to index the new app. Try opening it from Applications in Finder instead. Look for "DB Browser for SQLite" in the list.
help me find and open DB Browser for SQLite after installing it with Homebrew
Create a new database
Now you will create the empty database file where all of ShopSmart's data will live. Think of a database as a container that holds multiple tables of organized data.
- In DB Browser, click New Database in the toolbar at the top.
- In the save dialog, navigate to your Desktop.
- Type shopmart.db as the file name.
- Click Save.
What is a .db file?
A .db file is a SQLite database file. It stores all your tables, data, and relationships in a single portable file on your computer.
- A "Create Table" dialog may appear automatically. Click Cancel to dismiss it for now.
You should now see the DB Browser window with shopmart.db in the title bar and an empty database with no tables listed.
Don't see shopmart.db in the title bar?
Make sure you saved to the Desktop and used the exact name shopmart.db. If the title bar shows "(untitled)" or nothing, go to File then Open Database and select shopmart.db from your Desktop.
help me confirm that my shopmart.db database was created correctly in DB Browser for SQLite
Your database is empty and ready for tables. Next up, you will create the tables that hold ShopSmart's customer, product, and order data.
Create Your Tables
Your empty database is ready. Now it's time to give it structure by creating the tables that will hold ShopSmart's data.
Every database needs a schema, which is the blueprint for how data is organized into tables and columns. You'll create four tables that work together: customers, products, orders, and order_items.
In this step, get ready to:
- Create the customers table to store customer information.
- Create the products and orders tables for inventory and purchases.
- Create the order_items table to track what is in each order.
Open the Execute SQL tab and create the customers table
SQL statements are how you communicate with a database. You'll type each command into the Execute SQL tab, which is the built-in query editor in DB Browser.
- In DB Browser for SQLite, click the Execute SQL tab near the top of the window (next to Database Structure).
- You should see a blank text editor area where you can type SQL.
- Create the customers table by pasting the following SQL into the editor:
-- Create the customers table
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
city TEXT NOT NULL,
state TEXT NOT NULL,
signup_date TEXT NOT NULL
);
What does this SQL do?
CREATE TABLE customers (...) tells SQLite to make a new table called customers. Each line inside the parentheses defines a column.
- INTEGER PRIMARY KEY AUTOINCREMENT makes customer_id a unique number that increases automatically for each new row.
- TEXT NOT NULL means the column stores text and cannot be left empty.
- REAL NOT NULL (used later) stores decimal numbers and cannot be left empty.
- Run the SQL by pressing Cmd+Return (macOS) or Ctrl+Return (Windows).
- You should see a confirmation message at the bottom of the editor with no errors.
Seeing an error message?
- Check that you copied the entire statement including the semicolon at the end. SQL statements must end with ; to run.
- Make sure there are no extra characters or spaces before CREATE.
- If you see "table customers already exists", you ran it twice. That is fine for now.
Help me debug my CREATE TABLE error.
Create the products and orders tables
Now you need tables to store what ShopSmart sells and the orders customers place. You'll add both in sequence.
- Clear the editor by selecting all the text and deleting it.
- Create the products table by pasting this SQL:
-- Create the products table
CREATE TABLE products (
product_id INTEGER PRIMARY KEY AUTOINCREMENT,
product_name TEXT NOT NULL,
category TEXT NOT NULL,
price REAL NOT NULL
);
What does REAL mean?
REAL is SQLite's data type for decimal numbers (like prices). It stores values such as 79.99 or 24.99. TEXT is for strings, and INTEGER is for whole numbers.
- Run the SQL by pressing Cmd+Return (macOS) or Ctrl+Return (Windows).
- Confirm you see a success message with no errors at the bottom of the editor.
- Clear the editor again, then paste the following SQL to create the orders table:
-- Create the orders table
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id INTEGER NOT NULL,
order_date TEXT NOT NULL,
total_amount REAL NOT NULL
);
Why is order_date stored as TEXT?
SQLite stores dates as text in ISO format (YYYY-MM-DD). This keeps things simple while still letting you sort and filter by date.
The customer_id column links each order back to the customer who placed it. This relationship between tables is what makes a database "relational."
- Run the SQL by pressing Cmd+Return (macOS) or Ctrl+Return (Windows).
- Confirm you see a success message with no errors.
Getting a 'table already exists' error?
This means you accidentally ran the same CREATE TABLE statement twice. You can safely ignore this error since the table was already created on the first run.
Help me understand this SQL error.
Create the order_items table and save your changes
The final table connects orders to products. Each row in order_items represents one product inside an order, including how many were purchased.
- Clear the editor, then paste the following SQL to create the order_items table:
-- Create the order_items table
CREATE TABLE order_items (
item_id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price REAL NOT NULL
);
Why do we need an order_items table?
A single order can contain multiple products. Instead of cramming all product details into the orders table, order_items stores each product-in-an-order as its own row.
This is a common pattern in relational databases called a "junction table." It connects orders to products through order_id and product_id.
- Run the SQL by pressing Cmd+Return (macOS) or Ctrl+Return (Windows).
- Confirm you see a success message with no errors.
- Click Write Changes in the toolbar at the top of the window to save all four tables to disk.
- Click the Database Structure tab. You should see all four tables listed: customers, orders, order_items, and products.
Don't see all four tables?
- Make sure you clicked Write Changes. Without this step, your tables exist only in memory and may not appear in the structure view.
- Check that each CREATE TABLE statement ran without errors. Switch back to the Execute SQL tab and re-run any that failed.
Help me figure out why my tables aren't showing up.
✔️ Awesome, I've got everything!
Great. Double check you've clicked Write Changes and can see all four tables in the Database Structure tab.
ⓧ I'd like to double check the full code
Here is the complete SQL for all four CREATE TABLE statements. Compare this against what you ran:
-- Create the customers table
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
city TEXT NOT NULL,
state TEXT NOT NULL,
signup_date TEXT NOT NULL
);
-- Create the products table
CREATE TABLE products (
product_id INTEGER PRIMARY KEY AUTOINCREMENT,
product_name TEXT NOT NULL,
category TEXT NOT NULL,
price REAL NOT NULL
);
-- Create the orders table
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id INTEGER NOT NULL,
order_date TEXT NOT NULL,
total_amount REAL NOT NULL
);
-- Create the order_items table
CREATE TABLE order_items (
item_id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price REAL NOT NULL
);
Your database now has a complete schema. Next up, you'll fill these tables with sample data so you have something to query.
Load Sample Data
Your database has four empty tables ready to hold ShopSmart's data. An empty database is like a filing cabinet with labeled drawers but nothing inside.
In this step, you'll populate those tables with realistic e-commerce data. You'll add customers, products, orders, and order items so you have something meaningful to query in later steps.
In this step, get ready to:
- Insert 10 customers with varied cities and signup dates.
- Insert 8 products across three categories.
- Insert 15 orders and 25 order items linking customers to products.
Insert 10 customers
The INSERT INTO statement adds new rows to a table. You specify which columns to fill and the values for each row.
- In the Execute SQL tab from earlier, clear any existing SQL by selecting all the text and deleting it.
- Add ShopSmart's 10 customers by pasting this SQL:
-- Insert customers
INSERT INTO customers (first_name, last_name, email, city, state, signup_date) VALUES
('Sarah', 'Johnson', 'sarah.j@email.com', 'Austin', 'TX', '2024-01-15'),
('Mike', 'Chen', 'mike.chen@email.com', 'Seattle', 'WA', '2024-02-20'),
('Emily', 'Davis', 'emily.d@email.com', 'Portland', 'OR', '2024-03-10'),
('James', 'Wilson', 'james.w@email.com', 'Austin', 'TX', '2024-04-05'),
('Lisa', 'Anderson', 'lisa.a@email.com', 'Denver', 'CO', '2024-05-12'),
('David', 'Martinez', 'david.m@email.com', 'Seattle', 'WA', '2024-06-18'),
('Rachel', 'Taylor', 'rachel.t@email.com', 'Portland', 'OR', '2024-07-22'),
('Kevin', 'Brown', 'kevin.b@email.com', 'Denver', 'CO', '2024-08-30'),
('Amanda', 'Garcia', 'amanda.g@email.com', 'Austin', 'TX', '2024-09-14'),
('Chris', 'Lee', 'chris.l@email.com', 'Seattle', 'WA', '2024-10-01');
What does this code do?
Each line inside the VALUES section is one customer. The values are listed in the same order as the column names in the parentheses after the table name.
Notice the customers span four states (TX, WA, OR, CO) and signed up across different months. This variety gives you interesting data to filter and group later.
- Click the Execute button (the play icon) or press Cmd+Enter (macOS) or Ctrl+Enter (Windows) to run the statement.
- You should see a confirmation message in the output panel below the SQL editor showing that 10 rows were inserted.
Seeing an error about the table?
If you see an error like "no such table: customers", make sure you ran the CREATE TABLE statements from Step 2 and clicked Write Changes before moving on.
If you see a "UNIQUE constraint failed" error, you may have run the INSERT twice. Go to the Browse Data tab and check if data already exists in the customers table.
Still stuck? Help me fix my INSERT INTO customers error.
Insert 8 products across three categories
ShopSmart sells items across Electronics, Clothing, and Home & Kitchen. Having multiple categories lets you practice grouping and filtering queries later.
- Clear the SQL editor, then paste the following to add 8 products:
-- Insert products
INSERT INTO products (product_name, category, price) VALUES
('Wireless Headphones', 'Electronics', 79.99),
('Cotton T-Shirt', 'Clothing', 24.99),
('Bluetooth Speaker', 'Electronics', 49.99),
('Running Shoes', 'Clothing', 89.99),
('Coffee Maker', 'Home & Kitchen', 64.99),
('Yoga Mat', 'Clothing', 34.99),
('Desk Lamp', 'Home & Kitchen', 29.99),
('Portable Charger', 'Electronics', 39.99);
What does this code do?
Each product has a name, a category, and a price stored as a REAL number (decimal). Prices range from $24.99 to $89.99 so you'll get meaningful results when you calculate averages and totals.
Notice you don't need to include product_id in the column list. The AUTOINCREMENT keyword handles that automatically.
- Click Execute to run the statement.
- You should see a confirmation that 8 rows were inserted.
Seeing a syntax error?
Check that each value row ends with a comma except the last one, which ends with a semicolon. A missing comma or an extra comma after the last row is the most common cause.
Need help? Debug my INSERT INTO products statement.
Insert orders and order items
Orders connect customers to their purchases. Each order belongs to one customer, and each order can contain multiple items.
- Clear the SQL editor, then paste the following to add 15 orders:
-- Insert orders
INSERT INTO orders (customer_id, order_date, total_amount) VALUES
(1, '2024-11-01', 104.98),
(2, '2024-11-03', 79.99),
(3, '2024-11-05', 89.99),
(1, '2024-11-08', 64.99),
(4, '2024-11-10', 154.97),
(5, '2024-11-12', 49.99),
(2, '2024-11-15', 119.98),
(6, '2024-11-18', 29.99),
(3, '2024-11-20', 74.98),
(1, '2024-11-22', 89.99),
(7, '2024-11-25', 104.98),
(4, '2024-11-27', 79.99),
(5, '2024-12-01', 64.99),
(2, '2024-12-03', 129.98),
(8, '2024-12-05', 39.99);
What does this code do?
The customer_id column links each order to a customer. For example, customer_id 1 (Sarah Johnson) has three orders. This is how relational databases connect data across tables.
Dates are stored as text in YYYY-MM-DD format. This is how SQLite handles dates, and the format makes sorting and filtering by date straightforward.
- Click Execute to run the statement.
- You should see a confirmation that 15 rows were inserted.
Now you need the order items. These track which specific products are in each order and how many of each were purchased.
- Clear the SQL editor, then paste the following to add 25 order items:
-- Insert order items
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES
(1, 1, 1, 79.99),
(1, 2, 1, 24.99),
(2, 1, 1, 79.99),
(3, 4, 1, 89.99),
(4, 5, 1, 64.99),
(5, 1, 1, 79.99),
(5, 6, 1, 34.99),
(5, 8, 1, 39.99),
(6, 3, 1, 49.99),
(7, 4, 1, 89.99),
(7, 7, 1, 29.99),
(8, 7, 1, 29.99),
(9, 2, 1, 24.99),
(9, 3, 1, 49.99),
(10, 4, 1, 89.99),
(11, 5, 1, 64.99),
(11, 8, 1, 39.99),
(12, 1, 1, 79.99),
(13, 5, 1, 64.99),
(14, 3, 1, 49.99),
(14, 1, 1, 79.99),
(15, 8, 1, 39.99),
(5, 2, 1, 24.99),
(7, 2, 1, 24.99),
(11, 2, 1, 24.99);
What does this code do?
Each row links an order to a product. The order_id tells you which order this item belongs to, and product_id tells you which product was purchased.
Notice that some orders have multiple items. Order 5 has three items (product_ids 1, 6, 8, and later 2). This is the power of a separate order_items table. One order can contain any number of products.
- Click Execute to run the statement.
- You should see a confirmation that 25 rows were inserted.
- Click Write Changes in the toolbar to save all the data to disk.
- Verify your data by clicking the Browse Data tab at the top.
- Use the table dropdown to switch between tables. You should see 10 customers, 8 products, 15 orders, and 25 order items.
Don't see any data in Browse Data?
Make sure you clicked Write Changes after running all four INSERT statements. Without this, your data only exists in memory and may not appear in the Browse Data tab.
If the table dropdown shows no tables at all, your database file may not have loaded correctly. Close and reopen shopmart.db from your Desktop using File > Open Database.
Still stuck? Help me see my inserted data in DB Browser for SQLite.
✔️ Awesome, I've got everything!
Great. Double check you've clicked Write Changes so everything is saved to shopmart.db.
ⓧ I'd like to double check the full code
Here is the complete 02_insert_data.sql file with all four INSERT statements:
-- Insert customers
INSERT INTO customers (first_name, last_name, email, city, state, signup_date) VALUES
('Sarah', 'Johnson', 'sarah.j@email.com', 'Austin', 'TX', '2024-01-15'),
('Mike', 'Chen', 'mike.chen@email.com', 'Seattle', 'WA', '2024-02-20'),
('Emily', 'Davis', 'emily.d@email.com', 'Portland', 'OR', '2024-03-10'),
('James', 'Wilson', 'james.w@email.com', 'Austin', 'TX', '2024-04-05'),
('Lisa', 'Anderson', 'lisa.a@email.com', 'Denver', 'CO', '2024-05-12'),
('David', 'Martinez', 'david.m@email.com', 'Seattle', 'WA', '2024-06-18'),
('Rachel', 'Taylor', 'rachel.t@email.com', 'Portland', 'OR', '2024-07-22'),
('Kevin', 'Brown', 'kevin.b@email.com', 'Denver', 'CO', '2024-08-30'),
('Amanda', 'Garcia', 'amanda.g@email.com', 'Austin', 'TX', '2024-09-14'),
('Chris', 'Lee', 'chris.l@email.com', 'Seattle', 'WA', '2024-10-01');
-- Insert products
INSERT INTO products (product_name, category, price) VALUES
('Wireless Headphones', 'Electronics', 79.99),
('Cotton T-Shirt', 'Clothing', 24.99),
('Bluetooth Speaker', 'Electronics', 49.99),
('Running Shoes', 'Clothing', 89.99),
('Coffee Maker', 'Home & Kitchen', 64.99),
('Yoga Mat', 'Clothing', 34.99),
('Desk Lamp', 'Home & Kitchen', 29.99),
('Portable Charger', 'Electronics', 39.99);
-- Insert orders
INSERT INTO orders (customer_id, order_date, total_amount) VALUES
(1, '2024-11-01', 104.98),
(2, '2024-11-03', 79.99),
(3, '2024-11-05', 89.99),
(1, '2024-11-08', 64.99),
(4, '2024-11-10', 154.97),
(5, '2024-11-12', 49.99),
(2, '2024-11-15', 119.98),
(6, '2024-11-18', 29.99),
(3, '2024-11-20', 74.98),
(1, '2024-11-22', 89.99),
(7, '2024-11-25', 104.98),
(4, '2024-11-27', 79.99),
(5, '2024-12-01', 64.99),
(2, '2024-12-03', 129.98),
(8, '2024-12-05', 39.99);
-- Insert order items
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES
(1, 1, 1, 79.99),
(1, 2, 1, 24.99),
(2, 1, 1, 79.99),
(3, 4, 1, 89.99),
(4, 5, 1, 64.99),
(5, 1, 1, 79.99),
(5, 6, 1, 34.99),
(5, 8, 1, 39.99),
(6, 3, 1, 49.99),
(7, 4, 1, 89.99),
(7, 7, 1, 29.99),
(8, 7, 1, 29.99),
(9, 2, 1, 24.99),
(9, 3, 1, 49.99),
(10, 4, 1, 89.99),
(11, 5, 1, 64.99),
(11, 8, 1, 39.99),
(12, 1, 1, 79.99),
(13, 5, 1, 64.99),
(14, 3, 1, 49.99),
(14, 1, 1, 79.99),
(15, 8, 1, 39.99),
(5, 2, 1, 24.99),
(7, 2, 1, 24.99),
(11, 2, 1, 24.99);
Your database is loaded with realistic e-commerce data. Next up, you'll write your first queries to pull insights out of this data.
Write Your First Queries
Your database is loaded with realistic e-commerce data. Now it's time to ask it questions.
In SQL, you retrieve data using the SELECT statement. Every query you write translates a business question into a precise request that the database can answer.
In this step, get ready to:
- Write SELECT queries to view all data and specific columns.
- Filter data using WHERE with different conditions.
- Sort and limit results with ORDER BY and LIMIT.
Write SELECT queries to retrieve data
The SELECT statement is the most common SQL command. It pulls data from a table and displays it as a results grid.
- In DB Browser, switch to the Execute SQL tab.
- Clear any existing SQL from the editor so you have a fresh workspace.
- Type the following queries into the editor:
-- View all customers
SELECT * FROM customers;
-- View just names and cities
SELECT first_name, last_name, city FROM customers;
-- Sort customers alphabetically by last name
SELECT first_name, last_name, city
FROM customers
ORDER BY last_name;
What do these queries do?
- SELECT * FROM customers retrieves every column and every row from the customers table. The * means "all columns."
- SELECT first_name, last_name, city retrieves only the columns you name. This is better practice than * because you get exactly what you need.
- ORDER BY last_name sorts the results alphabetically. Without it, rows come back in no guaranteed order.
- Highlight the first query (SELECT * FROM customers;) with your cursor.
- Press Cmd+Return (macOS) or Ctrl+Return (Windows) to execute it.
- You should see all 10 customers displayed in the results grid below the editor.
- Highlight and run each of the other two queries the same way to see their results.
Run one query at a time
When you have multiple queries in the editor, highlight the one you want to run before pressing Cmd+Return (macOS) or Ctrl+Return (Windows). If nothing is selected, DB Browser runs all queries at once.
Not seeing any results?
- Make sure you highlighted the query text before executing. The results panel only shows the output of the last executed statement.
- Check for a missing semicolon at the end of your query. Every SQL statement must end with ;.
- If you see "no such table: customers", your data from the previous step was not saved. Click Write Changes and try again.
Help me troubleshoot my SELECT query in DB Browser for SQLite.
Filter data with WHERE
SELECT retrieves rows, but in real analysis you rarely want all of them. The WHERE clause filters rows down to only those matching a condition.
- Add the following queries below your existing code in the editor:
-- Find customers in Texas
SELECT first_name, last_name, city
FROM customers
WHERE state = 'TX';
-- Find products under $50
SELECT product_name, category, price
FROM products
WHERE price < 50;
-- Find orders from November 2024
SELECT order_id, customer_id, order_date, total_amount
FROM orders
WHERE order_date >= '2024-11-01' AND order_date < '2024-12-01';
What do these filters do?
- WHERE state = 'TX' matches only rows where the state column equals the text value TX. Text values in SQL are always wrapped in single quotes.
- WHERE price < 50 uses a comparison operator. Numbers do not need quotes.
- AND combines two conditions so both must be true. The date query finds orders on or after November 1 and before December 1.
- Highlight and run the Texas customers query. You should see 3 rows (Sarah, James, and Amanda).
- Highlight and run the products under $50 query. You should see 4 products.
Now add queries that combine multiple conditions using AND and OR:
- Add these queries below your existing code:
-- Find expensive electronics (combining conditions)
SELECT product_name, price
FROM products
WHERE category = 'Electronics' AND price > 40;
-- Find products that are Electronics OR under $30
SELECT product_name, category, price
FROM products
WHERE category = 'Electronics' OR price < 30;
AND vs OR
AND narrows results because both conditions must be true. OR widens results because only one condition needs to be true.
The first query returns only electronics priced above $40 (2 products). The second returns anything that is either an electronic or costs less than $30 (5 products).
- Highlight and run each query to confirm the results match.
Getting zero rows back?
- Check your string values are wrapped in single quotes, not double quotes. SQL uses 'Electronics' not "Electronics".
- Text comparisons are case-sensitive. 'electronics' will not match 'Electronics' in the data.
Help me fix my WHERE clause that returns zero rows.
Sort and limit results with ORDER BY and LIMIT
In data analysis, you often need to find the "top N" items. Combining ORDER BY with DESC (descending) and LIMIT gives you exactly that.
- Add the following query below your existing code in the editor:
-- Top 5 most expensive products
SELECT product_name, category, price
FROM products
ORDER BY price DESC
LIMIT 5;
What does this query do?
- ORDER BY price DESC sorts products from highest price to lowest. Without DESC, the default is ascending (lowest first).
- LIMIT 5 cuts the results to only the first 5 rows. Combined with DESC, this gives you the top 5 most expensive products.
- Highlight and run this query. You should see Running Shoes ($89.99) at the top, followed by Wireless Headphones ($79.99).
- Save your SQL file by clicking the save icon in the Execute SQL toolbar, or pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Name the file 03_basic_queries.sql and save it to your Desktop.
Results not sorted correctly?
- Make sure you typed DESC after the column name. Without it, results sort in ascending order by default.
- Check that LIMIT comes after ORDER BY. SQL clauses must appear in a specific order: SELECT, FROM, WHERE, ORDER BY, LIMIT.
Help me fix my ORDER BY query that isn't sorting correctly.
✔️ Awesome, I've got everything!
Great. Double check you saved your file as 03_basic_queries.sql.
ⓧ I'd like to double check the full code
Here is the complete 03_basic_queries.sql file with all queries from this step:
-- View all customers
SELECT * FROM customers;
-- View just names and cities
SELECT first_name, last_name, city FROM customers;
-- Sort customers alphabetically by last name
SELECT first_name, last_name, city
FROM customers
ORDER BY last_name;
-- Find customers in Texas
SELECT first_name, last_name, city
FROM customers
WHERE state = 'TX';
-- Find products under $50
SELECT product_name, category, price
FROM products
WHERE price < 50;
-- Find orders from November 2024
SELECT order_id, customer_id, order_date, total_amount
FROM orders
WHERE order_date >= '2024-11-01' AND order_date < '2024-12-01';
-- Find expensive electronics (combining conditions)
SELECT product_name, price
FROM products
WHERE category = 'Electronics' AND price > 40;
-- Find products that are Electronics OR under $30
SELECT product_name, category, price
FROM products
WHERE category = 'Electronics' OR price < 30;
-- Top 5 most expensive products
SELECT product_name, category, price
FROM products
ORDER BY price DESC
LIMIT 5;
You can now retrieve, filter, sort, and limit data from any table. Next up, you will use aggregate functions to summarize data and answer bigger business questions.
Use Aggregate Functions and GROUP BY
You can now filter, sort, and limit rows from individual tables. Those are essential skills, but your manager at ShopSmart asks bigger-picture questions: How many orders came in last month? What's our average order value?
To answer these, you need aggregate functions. These are SQL functions that take many rows and collapse them into a single summary number. Combined with GROUP BY, they let you break summaries down by category, customer, or time period.
In this step, get ready to:
- Use COUNT, SUM, AVG, MIN, and MAX to summarize the orders table.
- Use GROUP BY to break down data by product category.
- Combine GROUP BY with ORDER BY and aliases to rank categories by revenue.
Summarize order data with aggregate functions
Individual rows are useful for detail, but management cares about totals and averages. SQL's aggregate functions crunch an entire column into one result.
- In DB Browser, click the Execute SQL tab.
- Clear any SQL left over from the previous step.
- Write the following aggregate queries to answer four business questions at once:
-- How many orders do we have?
SELECT COUNT(*) AS total_orders FROM orders;
-- What's our total revenue?
SELECT SUM(total_amount) AS total_revenue FROM orders;
-- What's our average order value?
SELECT ROUND(AVG(total_amount), 2) AS avg_order_value FROM orders;
-- What's our smallest and largest order?
SELECT
MIN(total_amount) AS smallest_order,
MAX(total_amount) AS largest_order
FROM orders;
What do these functions do?
- COUNT(*) counts the total number of rows.
- SUM(total_amount) adds up every value in that column.
- ROUND(AVG(...), 2) calculates the average and rounds to 2 decimal places.
- MIN() and MAX() find the smallest and largest values in the column.
- AS gives your result column a readable name (called an alias) instead of the raw function expression.
- Select all four queries and click the Execute SQL button (or press Cmd+Return on macOS, Ctrl+Return on Windows).
- You should see 15 total orders, a total revenue of approximately 1279.78, an average order value of 85.32, a smallest order of 29.99, and a largest order of 154.97.
Seeing an error or unexpected result?
- If you see a "no such table" error, make sure you ran the insert statements from the previous step and clicked Write Changes.
- If your numbers look wrong, confirm you're querying the orders table (not order_items).
- Make sure each query ends with a semicolon.
My aggregate queries aren't returning the expected numbers. Help me debug.
Group data by category with GROUP BY
Aggregate functions are powerful on their own, but your manager doesn't just want one total. They want breakdowns: how many products per category, what's the average price per category.
What is GROUP BY?
GROUP BY splits your table into groups based on a column's values, then runs the aggregate function separately on each group. Think of it like sorting papers into folders first, then counting how many pages are in each folder.
- Add a blank line after your last query, then write this GROUP BY query to count products per category:
-- Count products per category
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category;
What does this query do?
- GROUP BY category sorts all products into groups (one group per unique category value).
- COUNT(*) then counts the rows inside each group separately.
- The result is one row per category with its count.
- Highlight just this query and click Execute SQL.
- You should see three rows: Clothing with 3 products, Electronics with 3 products, and Home & Kitchen with 2 products.
Only seeing one row?
Make sure you highlighted only the GROUP BY query before executing. If you run all queries at once, DB Browser shows the result of the last one. Highlight the specific query you want to test.
My GROUP BY query is only returning one row instead of a breakdown by category.
Now let's find the average price per category. This helps ShopSmart understand pricing across product lines.
- Add this query below the previous one:
-- Average price per category
SELECT category, ROUND(AVG(price), 2) AS avg_price
FROM products
GROUP BY category;
What does this query do?
This query groups products by category, then calculates the average price within each group. ROUND(..., 2) keeps the result to two decimal places so prices look clean.
- Highlight this query and execute it.
- You should see Clothing at 49.99, Electronics at 56.66, and Home & Kitchen at 47.49.
Seeing long decimal numbers?
If your averages show many decimal places, make sure you wrapped AVG(price) inside ROUND(..., 2). The 2 tells SQL how many decimal places to keep.
Help me fix my ROUND function so my average prices show only two decimal places.
Rank categories by revenue with GROUP BY and ORDER BY
Your manager's biggest question: which product categories make the most money? This query combines everything you've learned so far. It joins two tables, groups the results, and sorts them by revenue.
- Add this query below your previous ones:
-- Total revenue per category (using order_items + products)
SELECT
p.category,
SUM(oi.quantity * oi.price) AS category_revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.category
ORDER BY category_revenue DESC;
What does this query do?
- JOIN products p ON oi.product_id = p.product_id connects order items to their product details so we can access the category column.
- SUM(oi.quantity * oi.price) multiplies each item's quantity by its price, then sums up the totals per group.
- GROUP BY p.category groups the joined rows by product category.
- ORDER BY category_revenue DESC sorts the results from highest revenue to lowest.
- AS category_revenue gives the calculated column a clear alias you can reference in ORDER BY.
What are table aliases?
The oi and p after the table names are table aliases. They're shortcuts so you can write oi.price instead of order_items.price. This keeps multi-table queries readable.
- Highlight this query and execute it.
- You should see Electronics at the top with the highest revenue, followed by Clothing, then Home & Kitchen.
Seeing a "no such column" error?
- Make sure you included AS category_revenue in the SELECT line. Without the alias, ORDER BY category_revenue has nothing to reference.
- Check that your JOIN line matches exactly: JOIN products p ON oi.product_id = p.product_id.
- Verify that the table alias oi appears after FROM order_items in your query.
My revenue-by-category query with JOIN and GROUP BY is throwing an error. Help me debug it.
- Save this file as 04_aggregates_groupby.sql by clicking Save SQL file (the floppy disk icon above the SQL editor) and saving it to your Desktop.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete 04_aggregates_groupby.sql file for comparison:
-- How many orders do we have?
SELECT COUNT(*) AS total_orders FROM orders;
-- What's our total revenue?
SELECT SUM(total_amount) AS total_revenue FROM orders;
-- What's our average order value?
SELECT ROUND(AVG(total_amount), 2) AS avg_order_value FROM orders;
-- What's our smallest and largest order?
SELECT
MIN(total_amount) AS smallest_order,
MAX(total_amount) AS largest_order
FROM orders;
-- Count products per category
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category;
-- Average price per category
SELECT category, ROUND(AVG(price), 2) AS avg_price
FROM products
GROUP BY category;
-- Total revenue per category (using order_items + products)
SELECT
p.category,
SUM(oi.quantity * oi.price) AS category_revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.category
ORDER BY category_revenue DESC;
You can now summarize entire tables into single numbers and break those summaries down by category. Next up, you'll use JOINs to combine data across multiple tables and answer even richer business questions.
Join Tables Together
Your aggregate queries can summarize a single table. But real business questions rarely live in one table alone.
Right now, the orders table only stores a customer_id number. To answer "Who placed this order?" you need to combine orders with customers. That's what JOINs do.
In this step, get ready to:
- Write an INNER JOIN to combine orders with customer names.
- Use a LEFT JOIN to find customers who never ordered.
- Join three tables together to see full order details.
Write an INNER JOIN to show who placed each order
An INNER JOIN connects rows from two tables where a shared column matches. It only returns rows that have a match in both tables.
- In DB Browser, click the **Execute SQL** tab.
- Click the **Open Tab** button (the small tab icon with a "+") to create a fresh SQL tab.
- Write your first JOIN query by pasting this code:
-- INNER JOIN: Show who placed each order
SELECT
o.order_id,
c.first_name,
c.last_name,
o.order_date,
o.total_amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
ORDER BY o.order_date;
What does this query do?
The FROM orders o line starts with the orders table and gives it a short alias o. The alias lets you write o.order_id instead of orders.order_id every time.
The INNER JOIN customers c ON o.customer_id = c.customer_id line tells SQL to match each order to its customer using the shared customer_id column. Only orders with a matching customer (and customers with a matching order) appear in the results.
- Click the **Execute** button (the play icon) or press **F5** to run the query.
- You should see 15 rows in the results pane, each showing an order with the customer's first and last name attached.
Seeing an error about ambiguous columns?
Make sure every column name has its table alias prefix (like o.order_id or c.first_name). When two tables share a column name like customer_id, SQL does not know which table you mean unless you specify.
Help me fix my INNER JOIN query
Use a LEFT JOIN to find customers with no orders
A LEFT JOIN returns all rows from the left table, even if there is no match in the right table. Unmatched rows get NULL values for the right table's columns.
This makes it perfect for finding customers who have NOT placed any orders. Interviewers call this the "anti-join" pattern and it comes up constantly in data analyst interviews.
- Clear the SQL editor, then paste this query below your previous one (or add it on a new line after a blank line):
-- LEFT JOIN: Find customers who have NOT placed any orders
SELECT
c.first_name,
c.last_name,
c.email,
o.order_id
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
How does the anti-join pattern work?
The LEFT JOIN keeps every customer, even if they have no matching order. Customers without orders get NULL in the order_id column.
The WHERE o.order_id IS NULL filter then keeps only those unmatched rows. The result is a list of customers who have never placed an order.
- Highlight just the LEFT JOIN query and click **Execute** (or press **F5**).
- You should see 2 rows: Amanda Garcia and Chris Lee. These are the only customers who have never placed an order.
Seeing all 10 customers instead of 2?
Make sure you included the WHERE o.order_id IS NULL line at the end. Without that filter, the LEFT JOIN returns every customer (including those with orders).
Help me understand why my LEFT JOIN is returning too many rows
Join three tables to see full order details
Real analysis often requires data from more than two tables. To answer "What products were ordered and in what quantities?" you need to connect order_items to both orders and products.
- Add this three-table JOIN query below your previous queries:
-- Join three tables: What products were ordered?
SELECT
o.order_id,
o.order_date,
p.product_name,
p.category,
oi.quantity,
oi.price
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
JOIN products p ON oi.product_id = p.product_id
ORDER BY o.order_date, o.order_id;
What does this query do?
This query starts from order_items (aliased as oi) and chains two JOINs. The first JOIN connects to orders to get the order date. The second JOIN connects to products to get the product name and category.
Table aliases (oi, o, p) keep the query readable when you reference columns from multiple tables. Without them, you would need to write out the full table name every time.
- Highlight the three-table JOIN query and click **Execute**.
- You should see 25 rows showing every item that was ordered, with the product name, category, quantity, and price for each.
Getting fewer than 25 rows?
Check that you have both JOIN lines. If you only have one, the query connects to only one of the two tables and misses data. Also make sure you ran the full data insert from Step 3 (all 25 order items).
Help me debug my three-table JOIN
- Save your SQL tab as 05_joins.sql by clicking **File** then **Save** (or press **Ctrl+S** on Windows, **Cmd+S** on macOS).
✔️ Awesome, I've got everything!
Great. Double check you have saved your 05_joins.sql file.
ⓧ I'd like to double check the full code
Here is the complete 05_joins.sql file with all three queries:
-- INNER JOIN: Show who placed each order
SELECT
o.order_id,
c.first_name,
c.last_name,
o.order_date,
o.total_amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
ORDER BY o.order_date;
-- LEFT JOIN: Find customers who have NOT placed any orders
SELECT
c.first_name,
c.last_name,
c.email,
o.order_id
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
-- Join three tables: What products were ordered?
SELECT
o.order_id,
o.order_date,
p.product_name,
p.category,
oi.quantity,
oi.price
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
JOIN products p ON oi.product_id = p.product_id
ORDER BY o.order_date, o.order_id;
You can now pull data from any combination of tables in your database. Next up, you will put JOINs together with GROUP BY and aggregates to answer real business questions like "Who are our top customers?" and "Which category drives the most revenue?"
Answer Business Questions
You now know how to connect tables together with JOINs. That's the key that unlocks real business analysis.
In this step, you'll combine everything you've learned so far to answer real questions your manager might ask. These queries use JOIN, GROUP BY, SUM, ORDER BY, and LIMIT together in a single statement.
In this step, get ready to:
- Find the top 5 customers by total spending.
- Calculate revenue broken down by product category.
- Identify which month had the highest revenue.
Find the top 5 customers by total spending
Your manager wants to know: "Who are our best customers?" Answering this requires combining data from two tables (orders and customers), summing up each customer's spending, then sorting to find the top 5.
- In DB Browser, switch to the Execute SQL tab.
- Clear any existing SQL and paste this query to find the top customers by spending:
-- Top 5 customers by total spending
SELECT
c.first_name,
c.last_name,
SUM(o.total_amount) AS total_spent
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
ORDER BY total_spent DESC
LIMIT 5;
What does this query do?
- JOIN customers c ON o.customer_id = c.customer_id connects orders to the customer who placed them.
- GROUP BY c.customer_id, c.first_name, c.last_name groups all orders by each individual customer.
- SUM(o.total_amount) AS total_spent adds up every order for each customer and labels the result total_spent.
- ORDER BY total_spent DESC LIMIT 5 sorts highest to lowest and keeps only the top 5.
- Click the Execute button (or press F5) to run the query.
You should see a results table with 5 rows. Sarah Johnson should appear at the top with the highest total spending.
Why this query matters in interviews
This single query uses JOIN, GROUP BY, an aggregate function (SUM), ORDER BY, and LIMIT. Interviewers love "top N" questions because they test whether you can combine all of these clauses correctly in one statement.
Seeing an error or unexpected results?
- Make sure you include c.customer_id in the GROUP BY clause. Without it, customers who share the same name would be merged into one row.
- Check that you typed DESC after ORDER BY. Without it, the lowest spenders appear first instead of the highest.
Help me debug my top customers query
Calculate revenue by product category
Next, your manager asks: "Which product categories drive the most revenue?" This requires joining order_items with products to connect sales data to category labels.
- Clear the SQL editor and paste this query to calculate revenue by category:
-- Revenue by product category
SELECT
p.category,
SUM(oi.quantity * oi.price) AS total_revenue,
COUNT(oi.item_id) AS items_sold
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.category
ORDER BY total_revenue DESC;
What does this query do?
- SUM(oi.quantity * oi.price) multiplies the quantity by the price for each line item, then totals it per category. This gives accurate revenue even when someone orders multiple of the same product.
- COUNT(oi.item_id) AS items_sold counts how many individual line items were sold in each category.
- The query joins order_items to products because order_items has the sales data but products has the category names.
- Click Execute to run the query.
You should see 3 rows, one for each category (Electronics, Clothing, Home & Kitchen), ranked by total revenue from highest to lowest.
Revenue numbers look off?
- Make sure you're using oi.price (from order_items) and not p.price (from products). The order_items price reflects what the customer actually paid.
- Check that you have GROUP BY p.category. Without it, all rows collapse into one total instead of splitting by category.
Help me fix my revenue by category query
Find which month had the highest revenue
Your final question: "Which month performed best?" Since SQLite stores dates as text in YYYY-MM-DD format, you'll use SUBSTR() to extract just the year and month portion.
- Clear the SQL editor and paste this query to calculate monthly revenue:
-- Monthly revenue
SELECT
SUBSTR(order_date, 1, 7) AS month,
COUNT(*) AS order_count,
ROUND(SUM(total_amount), 2) AS monthly_revenue
FROM orders
GROUP BY SUBSTR(order_date, 1, 7)
ORDER BY month;
What does SUBSTR do here?
SUBSTR(order_date, 1, 7) extracts the first 7 characters of the date string. For a date like 2024-11-15, it returns 2024-11. This lets you group all orders from the same month together.
ROUND(SUM(total_amount), 2) rounds the total to 2 decimal places, giving you a clean dollar amount.
- Click Execute to run the query.
You should see 2 rows showing November 2024 and December 2024, each with an order count and total revenue.
Seeing only one row or a NULL month?
- Make sure the SUBSTR(order_date, 1, 7) expression appears in both your SELECT and your GROUP BY. They must match exactly.
- Check that the column name is order_date (not date or orderdate).
Help me fix my monthly revenue query
- Now save all three queries together. Click Save in the SQL editor toolbar (or press Ctrl+S on Windows, Cmd+S on macOS).
- Name the file 06_business_analysis.sql and save it to your Desktop.
- Click Write Changes in the DB Browser toolbar to save your database.
Saving your SQL files
Copy all three queries (top customers, category revenue, and monthly revenue) into a single file before saving. This gives you a portfolio piece you can reference during interview prep or share with a hiring manager.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file and clicked Write Changes.
ⓧ I'd like to double check the full code
Here is the complete 06_business_analysis.sql file with all three queries:
-- Top 5 customers by total spending
SELECT
c.first_name,
c.last_name,
SUM(o.total_amount) AS total_spent
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
ORDER BY total_spent DESC
LIMIT 5;
-- Revenue by product category
SELECT
p.category,
SUM(oi.quantity * oi.price) AS total_revenue,
COUNT(oi.item_id) AS items_sold
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.category
ORDER BY total_revenue DESC;
-- Monthly revenue
SELECT
SUBSTR(order_date, 1, 7) AS month,
COUNT(*) AS order_count,
ROUND(SUM(total_amount), 2) AS monthly_revenue
FROM orders
GROUP BY SUBSTR(order_date, 1, 7)
ORDER BY month;
You just answered three real business questions using SQL. These are the exact patterns that data analyst interviews test: combining JOINs with GROUP BY, aggregate functions, and sorting to extract insights from raw data. If you're ready for a bonus challenge, the Secret Mission takes it further with subqueries and HAVING.
Secret mission
Subqueries and HAVING
You've mastered the SQL fundamentals. Now it's time to level up with the techniques that separate junior from senior candidates in data analyst interviews. Write advanced queries using subqueries and the HAVING clause to answer questions that basic SQL can't handle.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources on your machine, or delete them entirely.
Resources you used:
- shopmart.db database file on your Desktop.
- Seven SQL query files (01_create_tables.sql through 07_advanced_queries.sql).
- DB Browser for SQLite (installed via Homebrew).
Keep everything running
No action needed. Choose this if you plan to keep practicing SQL, adding more tables, or extending your queries. Your database and SQL files stay on your Desktop.
Pause - I'll come back to this later
Since this project is entirely local files, there is nothing to pause. Your database and SQL files sit on your Desktop until you need them again.
- Close DB Browser for SQLite.
Delete - I don't want to use this again
Remove all project files and optionally uninstall DB Browser for SQLite.
- Delete the database and SQL files by running this command in Terminal:
- Uninstall DB Browser for SQLite by running:
rm ~/Desktop/shopmart.db ~/Desktop/01_create_tables.sql ~/Desktop/02_insert_data.sql ~/Desktop/03_basic_queries.sql ~/Desktop/04_aggregates_groupby.sql ~/Desktop/05_joins.sql ~/Desktop/06_business_analysis.sql ~/Desktop/07_advanced_queries.sql
Then uninstall DB Browser for SQLite:
brew uninstall db-browser-for-sqlite
- Confirm the files are gone by running:
ls ~/Desktop/shopmart.db
You should see a "No such file or directory" message.
Nice Work!
Nice Work!
You just went from zero SQL to writing multi-table analytical queries. You built a real e-commerce database from scratch and used it to answer the exact business questions that data analyst interviews test.
Here's what you learned:
- Created a relational database with 4 connected tables, then used SELECT, WHERE, ORDER BY, and LIMIT to retrieve and filter data.
- Used aggregate functions (COUNT, SUM, AVG, MIN, MAX) with GROUP BY to summarize data by category, customer, and time period.
- Wrote INNER JOINs and LEFT JOINs to combine data across tables and answer real business questions like top customers, revenue by category, and monthly trends.
- Used subqueries and HAVING to write advanced analytical queries that find VIP customers and underperforming products.
Ready to quiz yourself?