DetailsSales & Customer Analysis with SQL and Excel
Unlock all steps
Contents0/2 read
  1. 1Meet the data and load it
  2. 2Check and clean the data
  3. Build a sales view and monthly revenue
  4. Measure growth with window functions
  5. Order value and top products
  6. Gross margin by category
  7. Do customers come back? Cohort retention
  8. Segment customers with RFM
  9. Compare marketing channels and cities
  10. Monthly report view and Excel dashboard
Data solution

Sales & Customer Analysis with SQL and Excel

Turn a year of raw orders into a monthly report, product and margin analysis, customer cohorts and RFM segments: 10 steps, real data, every query included.

10 steps · 6 code snippets · Excel, PostgreSQL, Google Sheets

Step 1 · Free preview

Meet the data and load it

Kora Home & Living sells kitchenware, appliances, bedding, decor and bathroom items online across six Nigerian cities. You have four files: products (what we sell, at what price and cost), customers (when they signed up, where they live and how they found us), orders (who ordered, when, and whether it was completed, cancelled or refunded) and order_items (what was in each order). One order has one or more order lines.

Create the tables

01-create-tables.sql
CREATE TABLE products (
  product_id   text PRIMARY KEY,
  product_name text NOT NULL,
  category     text NOT NULL,
  unit_price   numeric(12,2) NOT NULL CHECK (unit_price >= 0),
  unit_cost    numeric(12,2) NOT NULL CHECK (unit_cost >= 0)
);

CREATE TABLE customers (
  customer_id         text PRIMARY KEY,
  signup_date         date NOT NULL,
  city                text,
  acquisition_channel text NOT NULL
);

CREATE TABLE orders (
  order_id    text PRIMARY KEY,
  customer_id text NOT NULL REFERENCES customers (customer_id),
  order_date  date NOT NULL,
  status      text NOT NULL CHECK (status IN ('completed', 'cancelled', 'refunded'))
);

-- No primary key yet: step 2 removes duplicate lines first.
CREATE TABLE order_items (
  order_id   text NOT NULL REFERENCES orders (order_id),
  product_id text NOT NULL REFERENCES products (product_id),
  quantity   integer NOT NULL CHECK (quantity > 0),
  unit_price numeric(12,2) NOT NULL CHECK (unit_price >= 0)
);

Import the CSV files

02-import.psql
\copy products    FROM 'products.csv'    WITH (FORMAT csv, HEADER true)
\copy customers   FROM 'customers.csv'   WITH (FORMAT csv, HEADER true)
\copy orders      FROM 'orders.csv'      WITH (FORMAT csv, HEADER true)
\copy order_items FROM 'order_items.csv' WITH (FORMAT csv, HEADER true)

Using pgAdmin or Supabase instead?

The \copy lines run in psql. In pgAdmin, right-click each table, choose Import/Export Data, pick the CSV and turn on Header. In Supabase, open the table editor and use Insert, then Import data from CSV. Import in the order shown: products and customers before orders, orders before order_items.

03-check-counts.sql
SELECT 'products' AS table_name, COUNT(*) AS row_count FROM products
UNION ALL SELECT 'customers', COUNT(*) FROM customers
UNION ALL SELECT 'orders', COUNT(*) FROM orders
UNION ALL SELECT 'order_items', COUNT(*) FROM order_items;

You should see

products 15, customers 420, orders 1,084, order_items 1,835. If a count is lower, the import stopped on an error: read the message, fix it and import that file again.

Step 2 · Free preview

Check and clean the data

Never analyse data you have not checked. Real exports contain duplicate rows, blank fields and stray spaces, and each one quietly changes your totals. Here we test for three common problems, fix them, then add a primary key so the duplicate can never come back.

04-quality-checks.sql
-- 1. The same product twice in one order (should never happen)
SELECT order_id, product_id, COUNT(*) AS copies
FROM order_items
GROUP BY order_id, product_id
HAVING COUNT(*) > 1;

-- 2. Blank cities or cities with extra spaces
SELECT customer_id, '[' || city || ']' AS city_as_stored
FROM customers
WHERE city IS NULL OR city = '' OR city <> TRIM(city);

-- 3. Orders with no lines at all
SELECT o.order_id, o.status
FROM orders o
LEFT JOIN order_items i ON i.order_id = o.order_id
WHERE i.order_id IS NULL;

You should see

Check 1: order O10016 has product P012 twice. Check 2: customer C0058 has a blank city and C0131 has " Lagos " with spaces (the brackets make the spaces visible). Check 3: no rows.

05-fix.sql
-- Keep one copy of each duplicated line
DELETE FROM order_items a
USING order_items b
WHERE a.ctid > b.ctid
  AND a.order_id = b.order_id
  AND a.product_id = b.product_id;

-- Trim spaces; turn empty text into a real NULL (unknown)
UPDATE customers
SET city = NULLIF(TRIM(city), '')
WHERE city IS DISTINCT FROM NULLIF(TRIM(city), '');

-- Make duplicates impossible from now on
ALTER TABLE order_items ADD PRIMARY KEY (order_id, product_id);

Why the duplicate matters

The duplicate line adds ₦36,000 (3 wall clocks at ₦12,000) of January revenue that never happened. One row looks harmless; in a real export there can be thousands. ctid is PostgreSQL's internal row address, which lets you delete one of two identical rows.

06-status-mix.sql
SELECT status,
       COUNT(*) AS orders,
       ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct_of_orders
FROM orders
GROUP BY status
ORDER BY orders DESC;

You should see 994 completed orders (91.7%), 58 cancelled (5.4%) and 32 refunded (3.0%). From now on we count only completed orders as sales; cancelled and refunded orders brought in no money.

Keep reading

8 more steps

  1. Build a sales view and monthly revenue

    Create one reusable view of completed sales, then total revenue, orders and customers by month.

  2. Measure growth with window functions

    Add month-over-month growth and a running total to the monthly figures.

  3. Order value and top products

    Calculate average and median order value by quarter and find the products that drive revenue.

  4. Gross margin by category

    See which categories actually make money, and what the November discount cost.

  5. Do customers come back? Cohort retention

    Group customers by the month of their first purchase and track how many buy again.

  6. Segment customers with RFM

    Score every customer on recency, frequency and spend, and turn the scores into action groups.

  7. Compare marketing channels and cities

    Find which acquisition channels bring the most valuable customers, and where revenue comes from.

  8. Monthly report view and Excel dashboard

    Save a one-row-per-month report in the database, export it and build a KPI dashboard in Excel.

Buy once, keep it
$0.07One payment · lifetime access · no subscription
Want everything? All-Access is $29 / month