Skip to content

How to Find Customers Who Have Never Booked (Using LEFT JOIN)

    Hello ,

    Here’s a question that comes up in every business: “Who hasn’t done something yet?”

    Which customers signed up but never booked?
    Which products exist but have never been ordered?
    Which guides are on payroll but haven’t been assigned to a trip?

    These are some of the most valuable questions you can answer, because they reveal untapped opportunity. A customer who signed up but never booked isn’t a lost cause — they’re someone who was interested enough to create an account. That’s a warm lead sitting in your database.

    The trick is finding them. And that’s where LEFT JOIN comes in.

    The Business Problem

    Your marketing director says: “We have 1,000 customers in the system. How many have actually booked an expedition? And more importantly — who hasn’t?”

    With an INNER JOIN, you’d only see customers who have bookings. The ones who never booked would disappear from your results entirely. That’s a problem when the people you want to find are exactly the ones who are missing.

    The Query That Finds Who’s Missing

    Here’s what I ran against the Summit Adventures database (the fake adventure tourism company I created to help people learn business analytics):

    -- Find customers who have never booked an expedition
    SELECT 
        c.customer_id,
        c.first_name || ' ' || c.last_name AS customer_name,
        c.email,
        c.experience_level,
        c.city,
        c.state,
        c.created_at::date AS signup_date
    FROM customers c
        LEFT JOIN bookings b ON c.customer_id = b.customer_id
    WHERE b.booking_id IS NULL
    ORDER BY c.created_at DESC;

    For Summit Adventures, this reveals a few hundred customers who signed up but never completed a booking. That’s a meaningful portion of the customer base that marketing hasn’t reached effectively.

    The insight:

    Every one of these customers took the step of creating an account. They were interested. Something stopped them from booking — maybe price, timing, trip availability, or just getting distracted. A targeted email campaign to this group could convert even a small percentage into paying customers at nearly zero acquisition cost.

    How LEFT JOIN + IS NULL Works

    Here’s the key concept:

    INNER JOIN returns only rows where both tables have matching records. Customers without bookings are excluded.

    LEFT JOIN returns ALL rows from the left table (customers), whether or not they have matching rows in the right table (bookings). When there’s no match, the booking columns come back as NULL.

    INNER JOIN result:
    Customer A → Booking 101  ✅ (shown)
    Customer B → Booking 205  ✅ (shown)
    Customer C → (no booking)  ❌ (hidden)
    
    LEFT JOIN result:
    Customer A → Booking 101  ✅ (shown)
    Customer B → Booking 205  ✅ (shown)
    Customer C → NULL          ✅ (shown!)

    The `WHERE b.booking_id IS NULL` filter then keeps only the customers with no match — the ones who never booked.

    Three “Who’s Missing?” Patterns

    This same technique answers many different business questions:

    Pattern 1: Customers with no bookings (shown above)

    Pattern 2: Expeditions with no bookings

    Which products aren’t selling?

    -- Find expeditions that have never been booked
    SELECT 
        e.expedition_id,
        e.expedition_name,
        e.expedition_type,
        e.difficulty_level,
        e.base_price,
        e.location
    FROM expeditions e
        LEFT JOIN expedition_instances ei ON e.expedition_id = ei.expedition_id
        LEFT JOIN bookings b ON ei.instance_id = b.instance_id
    WHERE b.booking_id IS NULL
    ORDER BY e.expedition_type, e.expedition_name;

    If an expedition has never been booked, should you promote it differently, reprice it, or retire it?

    Pattern 3: Guides with no recent assignments

    Who’s available or underutilized?

    -- Find guides not assigned to any trips in the last 6 months
    SELECT 
        g.guide_id,
        g.first_name || ' ' || g.last_name AS guide_name,
        g.specializations,
        g.years_experience,
        g.hourly_rate
    FROM guides g
        LEFT JOIN guide_assignments ga ON g.guide_id = ga.guide_id
            AND ga.assigned_date >= CURRENT_DATE - INTERVAL '6 months'
    WHERE ga.assignment_id IS NULL
        AND g.is_active = TRUE
    ORDER BY g.years_experience DESC;

    Notice the date filter is in the JOIN condition, not the WHERE clause. This is important — putting it in WHERE would filter out the NULLs we’re looking for.

    The Subtle Mistake That Trips People Up

    This is worth highlighting because it catches even experienced analysts:

    -- ❌ WRONG: Date filter in WHERE removes the NULLs you want
    SELECT g.guide_id, g.first_name
    FROM guides g
        LEFT JOIN guide_assignments ga ON g.guide_id = ga.guide_id
    WHERE ga.assigned_date >= CURRENT_DATE - INTERVAL '6 months'
        AND ga.assignment_id IS NULL;  -- This will NEVER return rows!

    If `assignment_id IS NULL`, then `assigned_date` is also NULL. And `NULL >= anything` evaluates to false. So the WHERE clause contradicts itself.

    -- ✅ RIGHT: Date filter in the JOIN condition
    SELECT g.guide_id, g.first_name
    FROM guides g
        LEFT JOIN guide_assignments ga ON g.guide_id = ga.guide_id
            AND ga.assigned_date >= CURRENT_DATE - INTERVAL '6 months'
    WHERE ga.assignment_id IS NULL;

    Moving the date filter into the ON clause means: “Try to find assignments in the last 6 months. If none exist, return NULL.” Then the WHERE clause correctly identifies guides with no recent assignments.

    This is one of those patterns that, once you see it, becomes straightforward. But it’s worth understanding why it works.

    Combining “Who’s Missing” With Segmentation

    You can make this even more useful by adding context to the missing customers:

    -- Never-booked customers segmented by experience level
    SELECT 
        c.experience_level,
        COUNT(*) AS never_booked_count,
        ROUND(COUNT(*) * 100.0 / (
            SELECT COUNT(*) FROM customers
        ), 1) AS pct_of_total_customers
    FROM customers c
        LEFT JOIN bookings b ON c.customer_id = b.customer_id
    WHERE b.booking_id IS NULL
    GROUP BY c.experience_level
    ORDER BY never_booked_count DESC;

    Now you know not just who hasn’t booked, but what kind of customer they are. If most never-booked customers are beginners, maybe the issue is that your expedition offerings skew too advanced. If they’re experts, maybe your pricing doesn’t match their expectations.

    This turns a simple “who’s missing” list into a strategic conversation about product-market fit.

    The Pattern to Remember

    -- Find records in Table A with no matching records in Table B
    SELECT a.*
    FROM table_a a
        LEFT JOIN table_b b ON a.id = b.a_id
    WHERE b.id IS NULL;

    Three lines of logic:
    1. LEFT JOIN — keep all records from the left table
    2. ON — define the relationship
    3. WHERE IS NULL — filter to only the unmatched ones

    This works for any “who hasn’t done X” question.

    Try This At Your Job

    Think about the absences in your data:

    E-commerce: Registered users who haven’t purchased
    SaaS: Users who signed up but haven’t activated a feature
    HR: Employees who haven’t completed required training
    Education: Students enrolled but haven’t submitted assignments

    The people who are missing from your results are often the ones who represent the biggest opportunity.

    Until next time,
    Brian ([say hi on twitter!](https://twitter.com/briangraves))

    P.S. Finding what’s missing is the core concept behind the Empty Stadium Blueprint in Module 5 of SQL for Business Impact. An empty seat represents lost revenue — and LEFT JOIN is how you find those empty seats. Check it out at [sqlforbusinessimpact.com](https://sqlforbusinessimpact.com).

    P.P.S. What “who hasn’t done X?” question would be most valuable at your company right now? Hit reply and tell me — these are some of my favorite problems to solve. I read every response.


    Want More SQL Insights Like This?

    Join thousands of analysts getting weekly tips on turning data into business impact.

    Subscribe to Analytics in Action →

    Leave a Reply

    Your email address will not be published. Required fields are marked *