NewThe ManyPI MCP Server is now live

Fictional B2B lead list for data cleaning and SQL

156 fictional business contacts from 12 European countries in one CSV file, with deliberate quality problems: duplicates, missing emails, mixed country spellings, role addresses and an opt-out conflict. Ten spreadsheet and SQL tasks with checked answers.

Dataset1 h 30 minBachelor +3CC0 1.0

What you will learn

  • Describe a lead list by its columns and spot quality problems
  • Standardise inconsistent values such as country names
  • Find duplicates that are not identical, with and without an email address
  • Respect opt-outs across duplicate records
  • Build a clean, send-ready list with filters or SQL

Before you start: Basic spreadsheet skills (filters, COUNTIF). For the SQL tasks: SELECT, WHERE and GROUP BY.

A lead list is a table of companies and people a sales team might contact. Real lead lists are rarely clean: the same person is entered twice, countries are spelled three ways, and some rows hold a shared address such as info@ instead of a person. This file has all of these problems on purpose, so learners can practise finding and fixing them before anyone sends a single email.

Everything in it is invented. Company names reuse well-known fictional sample names (Contoso, Fabrikam, Northwind and others) with an industry word, so no row can be mistaken for a real firm. Websites and email addresses end in .example, a domain that cannot exist. Phone numbers are made up: do not dial them.

What is in the file

fictional-b2b-leads.csv (in the Downloads on this page): 156 rows, 17 columns, comma-separated, UTF-8. One row is one contact at one company, as a sales team might have collected them between November 2025 and October 2026.

Column

Type

Meaning

Example

Problems you will meet

lead_id

text

Row number given by the system

L001

None, every row has its own

company

text

Company name with legal form

Tailspin Dental GmbH

One company appears with and without its legal form

industry

text

One of 12 industries

Healthcare

None

country

text

Country of the company

Germany

27 different spellings for 12 countries

city

text

City, English spelling

Munich

None

employees

number

Number of employees

25

5 rows are empty

founded

number

Year the company was founded

1976

None

website

text

Company domain

tailspin-dental.example

One row starts with www.

contact_first_name

text

First name of the contact

Aylin

Empty where the row is a role address

contact_last_name

text

Last name of the contact

Becker

Empty where the row is a role address

role

text

Job title of the contact

Head of Operations

Empty where the row is a role address

email

text

Email address

aylin.becker@tailspin-dental.example

13 empty, 12 role addresses, mixed capitals

phone

text

Phone number, invented

+49 89 5550 5785

One copy is written without spaces

source

text

Where the lead came from

purchased list

None, 8 values

lead_status

text

new, contacted, replied, meeting, customer or disqualified

contacted

None

last_contacted

date

Date of the last contact, YYYY-MM-DD

2026-01-10

Empty when the status is new

opted_out

text

yes if the person asked not to be contacted again

no

Two copies of one person disagree

A role address is a shared mailbox such as info@, office@ or sales@ that belongs to a function, not to one person. An opt-out is a person's request to receive no more marketing messages.

How to use it

  • Spreadsheet: open the file in any spreadsheet program. If the program asks, choose comma as the separator and UTF-8 as the character set, so that names like Österreich survive. The formulas below use the column letters of the file: A is lead_id, D is country, L is email, Q is opted_out, and the data is in rows 2 to 157.

  • SQL: load the file into the SQL Query Playground. It runs in the browser and nothing is uploaded. There is only one table there, so FROM leads works whatever you call it. In other databases, import the file as a table called leads.

  • Two things to know about SQL here. The playground compares text without caring about capital letters; many databases do care. The queries below use LOWER(), so they give the same result everywhere. The playground also treats empty cells as NULL. If your database imports empty cells as empty text, write (email IS NULL OR email = '') instead of email IS NULL.

Tasks

Work in pairs. Write down each answer as a number and one sentence that explains it. Tasks 1 to 4 describe the problems, tasks 5 to 7 deal with duplicates and opt-outs, tasks 8 to 10 use the data for sales decisions.

  1. Size. How many rows does the file have? How many different websites are there, first as written, then after removing "www."? What does the difference between rows and websites tell you?

  2. Country spellings. List every value in the country column with how often it appears. How many different values are there? How many leads are in Germany once all spellings are counted together?

  3. Missing values. How many rows have no email address? How many have no employee count? Which of the two gaps matters more for an email campaign?

  4. Role addresses. How many emails start with info@, office@, sales@, contact@ or hello@? What do these rows have in common apart from the email?

  5. Duplicate emails. Which email addresses appear more than once once you ignore capital letters? How many extra rows do they cause?

  6. Duplicates without an email. Task 5 misses one person, because one of their rows has no email. Find all people who appear twice by comparing first name, last name and website instead.

  7. Opt-outs. How many rows say opted_out = yes? One person has two rows that disagree. Who is it, and what went wrong?

  8. Lead sources. For each source, count the leads and how many reached a meeting (status meeting or customer). Which source has the highest meeting rate? Why should you be careful with this comparison?

  9. Stale leads. How many leads have the status contacted or replied but were last contacted before 1 May 2026, so more than six months ago? How many have never been contacted?

  10. A send-ready list. Build a list for an email campaign with these rules: a personal email address (not empty, not a role address), nobody who opted out in any of their rows, no customers and nothing disqualified, and each email address only once. How many addresses are left?

Answers

Answers to tasks 1 to 4

1. Size

156 rows. 141 different websites as written, 140 after removing "www." (Fabrikam Couriers is in the list once as fabrikam-couriers.example and once as www.fabrikam-couriers.example). So there are 140 companies but 156 rows: some companies have two different contacts, and some people are in the list twice.

  • Spreadsheet: =COUNTA(A2:A157), =COUNTA(UNIQUE(H2:H157)) and =COUNTA(UNIQUE(SUBSTITUTE(H2:H157,"www.",""))).

Task 1
SELECT COUNT(*) AS rows_total,
       COUNT(DISTINCT website) AS sites_raw,
       COUNT(DISTINCT REPLACE(LOWER(website), 'www.', '')) AS sites_clean
FROM leads

2. Country spellings

27 different values for 12 countries. Germany is written five ways: Germany (39), DE (3), Deutschland (3), germany (2) and "Germany " with a space at the end (1). Counted together that is 48 leads in Germany. The other variants: United Kingdom, UK, Great Britain and England; Netherlands, The Netherlands, Holland and NL; Austria, AT and Österreich; Switzerland, CH and Schweiz; France and FR.

After cleaning: Germany 48, United Kingdom 22, France 11, Sweden 11, Switzerland 11, Netherlands 10, Austria 9, Italy 9, Belgium 8, Spain 8, Poland 5, Denmark 4. Together 156.

  • SQL GROUP BY shows all 27 values. Some spreadsheet programs ignore capital letters in UNIQUE and pivot tables and show 26, because Germany and germany merge. The trailing space still makes "Germany " a value of its own.

  • Fix: make a small mapping table (raw value, standard name), then add a column with VLOOKUP or XLOOKUP on the trimmed value.

  • Note: England and Great Britain are not the same as the United Kingdom. For a country column they all point to the same country, but the example shows why a fixed list of allowed values (or ISO codes such as DE, GB, NL) beats free text.

Task 2
SELECT country, COUNT(*) AS leads
FROM leads
GROUP BY country
ORDER BY leads DESC;

SELECT COUNT(*) AS germany
FROM leads
WHERE LOWER(TRIM(country)) IN ('germany', 'de', 'deutschland')

Run the two queries one at a time.

3. Missing values

13 rows have no email, 5 have no employee count. For an email campaign the missing emails matter more: those rows cannot be used at all. A missing employee count only makes a size filter less precise. Do not guess emails such as firstname.lastname@ to fill the gap: a wrong guess bounces, and an address the person never gave you raises data protection questions.

  • Spreadsheet: =COUNTBLANK(L2:L157) and =COUNTBLANK(F2:F157).

Task 3
SELECT COUNT(*) AS no_email FROM leads WHERE email IS NULL

4. Role addresses

12 rows: 5 info@, 2 office@, 2 sales@, 2 contact@ and 1 hello@. None of these rows has a first name, last name or role, because nobody knows who reads the mailbox. They are useful to reach a company, but not for a personal email, and you cannot tell whether one person or ten will read it.

  • Spreadsheet: =COUNTIF(L2:L157,"info@*") and the same for the other four, added together. COUNTIF ignores capital letters.

Task 4
SELECT lead_id, company, email
FROM leads
WHERE email LIKE 'info@%' OR email LIKE 'office@%' OR email LIKE 'sales@%'
   OR email LIKE 'contact@%' OR email LIKE 'hello@%'
Answers to tasks 5 to 7

5. Duplicate emails

7 addresses appear twice, so there are 7 extra rows. Each pair teaches something different:

Person

Rows

What differs

Maya Sommer

L123, L125

Nothing: an exact copy

Sofia Moreau

L075, L104

Capital letters in the email

Aylin Becker

L001, L116

Email in capitals and a new job title

Samir Kowalski

L053, L096

Company name without "Ltd"

Marek Yilmaz

L056, L151

Website with www. and a newer contact date

Samir Peeters

L038, L068

Phone without spaces, status moved to meeting

Sofia Steiner

L030, L083

One row says opted out, the other does not

When you merge a pair, keep the newest status, the newest contact date and the newest job title, and keep any opt-out.

  • Spreadsheet: add a helper column with =LOWER(L2), then a second one with =COUNTIF(R$2:R$157,R2)>1 and filter on TRUE: 14 rows, 7 pairs.

Task 5
SELECT LOWER(email) AS email_clean, COUNT(*) AS copies
FROM leads
WHERE email IS NOT NULL
GROUP BY LOWER(email)
HAVING copies > 1

6. Duplicates without an email

8 people appear twice: the 7 from task 5 plus Lotte Moreau at Fabrikam Physio (L029 and L042). Her row L029 has no email, so matching on email could not find it. Altogether the file holds 148 different contacts (156 rows minus 8 copies) at 140 companies.

Matching on names has its own risk: two different people can share a name. That is why the key here combines name and website. Eight companies, for example Coho Building and Trey Hotels, have two different contacts on purpose: same company, different person, so they are not duplicates. At Proseware Home and Garden, Hugo Jansen and Noah Jansen share a last name but are two people.

  • Spreadsheet: a helper column =I2&" "&J2&" "&SUBSTITUTE(H2,"www.",""), then COUNTIF as in task 5. Ignore rows with an empty last name.

Task 6
SELECT contact_first_name, contact_last_name,
       REPLACE(LOWER(website), 'www.', '') AS site,
       COUNT(*) AS copies
FROM leads
WHERE contact_last_name IS NOT NULL
GROUP BY contact_first_name, contact_last_name, REPLACE(LOWER(website), 'www.', '')
HAVING copies > 1

7. Opt-outs

10 rows say yes. The conflict is Sofia Steiner at Lamna Care: row L083 says she replied and opted out (last contact 22 September 2026), row L030 says no, and that row was contacted again on 25 October 2026. Because the opt-out was only stored on one copy, she was contacted after she had asked to stop.

Under the GDPR, a person can object to the use of their data for direct marketing at any time, and the data must then no longer be used for that purpose (Article 21(2) and (3)). In practice: an opt-out belongs to the person, not to one row, so it must survive every merge and every new import. Keep a separate suppression list of opted-out addresses and check every list against it.

  • Spreadsheet: =COUNTIF(Q2:Q157,"yes"). Then filter the duplicate pairs from task 5 and compare column Q.

Task 7
SELECT LOWER(email) AS email_clean, COUNT(*) AS copies,
       SUM(opted_out = 'yes') AS opt_outs
FROM leads
WHERE email IS NOT NULL
GROUP BY LOWER(email)
HAVING copies > 1 AND opt_outs > 0

opted_out = 'yes' is 1 when true and 0 when false, so SUM counts the yes rows in each group.

Answers to tasks 8 to 10

8. Lead sources

Counted on the raw file, with meeting or customer as "reached a meeting":

Source

Leads

Reached a meeting

Meeting rate

website form

24

9

37.5 %

company register

16

5

31.3 %

job ad

19

5

26.3 %

referral

17

4

23.5 %

purchased list

38

8

21.1 %

trade fair

20

3

15.0 %

webinar

11

1

9.1 %

partner

11

1

9.1 %

Website forms have the highest rate. Be careful: the groups are small (one more meeting for webinars would raise their rate from 9.1 to 18.2 %), the file still contains duplicates, and many leads are still open. The data is fictional, so it says nothing about real sources. A fair comparison also needs the cost per source and enough leads in each group.

Task 8
SELECT source, COUNT(*) AS leads,
       SUM(lead_status IN ('meeting', 'customer')) AS meetings,
       ROUND(100.0 * SUM(lead_status IN ('meeting', 'customer')) / COUNT(*), 1) AS meeting_rate
FROM leads
GROUP BY source
ORDER BY meeting_rate DESC

9. Stale leads

43 leads: 30 contacted and 13 replied, all last contacted before 1 May 2026. 29 leads have the status new and have never been contacted (their last_contacted is empty). Contact data ages: people change jobs and companies move, so stale leads should be checked again before anyone writes to them.

  • Spreadsheet: =COUNTIFS(O2:O157,"contacted",P2:P157,"<"&DATE(2026,5,1)) plus the same for "replied". If your program keeps the dates as text, filter the column instead.

Task 9
SELECT COUNT(*) AS stale
FROM leads
WHERE lead_status IN ('contacted', 'replied')
  AND last_contacted < '2026-05-01'

Dates written as YYYY-MM-DD sort correctly even as text, which is one reason this format is used in data files.

10. A send-ready list

90 addresses. Step by step:

Step

Rows left

All rows

156

With an email address

143

Without role addresses

131

Without anyone who opted out in any row (10 rows plus Sofia Steiner's second row)

120

Without customers and disqualified leads

96

Each address once, ignoring capitals

90

The SQL below returns 90 rows. Without subqueries the playground cannot count them in the same query; read the number of rows it shows.

Task 10
SELECT LOWER(email) AS email_clean, COUNT(*) AS copies
FROM leads
WHERE email IS NOT NULL
  AND email NOT LIKE 'info@%' AND email NOT LIKE 'office@%'
  AND email NOT LIKE 'sales@%' AND email NOT LIKE 'contact@%'
  AND email NOT LIKE 'hello@%'
  AND lead_status NOT IN ('customer', 'disqualified')
GROUP BY LOWER(email)
HAVING SUM(opted_out = 'yes') = 0

The opt-out check sits in HAVING on purpose. A WHERE condition opted_out = 'no' would drop row L083 but keep row L030, and Sofia Steiner would be back on the list.

A clean list is not yet a lawful one. Whether you may email these people at all depends on where they are and how the data was collected: see where B2B leads come from.

Ideas for more

  • Statistics: the mean and median number of employees per industry, and why they differ so much (a few very large companies pull the mean up).

  • Data protection: 38 leads come from a purchased list. Write three questions you would ask the seller before using them, and say which GDPR principle each question protects.

  • Lead scoring: give every lead points for fit and activity with the lead scoring worksheet.

  • Deduplication tool: the free CSV Deduplicator runs in the browser. Compare its results on the email column with your answer to task 5.

License

The data and the tasks are released into the public domain under CC0 1.0. Use them however you like, no credit needed.

Downloads

Reuse this material

CC0 1.0

It's in the public domain: use it however you like. A credit with a link is appreciated, not required.

"Fictional B2B lead list for data cleaning and SQL" by ManyPI (https://manypi.com/edu/b2b-lead-list-dataset), licensed under CC0 1.0 (https://creativecommons.org/publicdomain/zero/1.0/)