HackerRank SQL (Intermediate) certification
Invoices Per Country
This is one of the questions in the HackerRank SQL (Intermediate) certification test. It was one of the two I got when I took the test in June 2022. The query below is tested on MySQL 8.0 and 5.7.
What the question asks
Every invoice belongs to a customer, every customer lives in a city, and every city is in a country. You have to work out the average invoice amount for each country and keep only the countries where that average is higher than the average of all invoices in the table.
For each country that qualifies, show three things:
- the country name
- how many invoices came from that country
- the average invoice amount for that country
I could not find a public copy of the full problem text for this one. The task above comes from the solutions in about a dozen GitHub repos (several are copies of each other, but the working ones all have the same shape) plus a code comment in one repo that describes the task. None of them sorts the output or asks for a specific rounding. If your version of the question gives an order, add an ORDER BY for it at the end.
Tables
Four tables are involved. Only the columns you need are listed here.
| Table | Columns used |
|---|---|
country |
id, country_name |
city |
id, city_name, country_id (points to country.id) |
customer |
id, customer_name, city_id (points to city.id) |
invoice |
id, customer_id (points to customer.id), total_price (decimal(8,2)) |
Solution
SELECT
co.country_name,
COUNT(i.id) AS invoice_count,
ROUND(AVG(i.total_price), 6) AS average_invoice
FROM country AS co
JOIN city AS ci ON ci.country_id = co.id
JOIN customer AS cu ON cu.city_id = ci.id
JOIN invoice AS i ON i.customer_id = cu.id
GROUP BY co.country_name
HAVING AVG(i.total_price) > (SELECT AVG(total_price) FROM invoice);How it works
- Walk the chain of joins. An invoice does not store a country. You get there in three steps: invoice to customer, customer to city, city to country. After the joins, every invoice row also carries its country name.
- Group by country.
GROUP BY co.country_nameturns all invoice rows of one country into a single row.COUNT(i.id)counts the invoices in that group andAVG(i.total_price)gives the country’s average invoice. - Compare with the overall average. The subquery
(SELECT AVG(total_price) FROM invoice)runs once and returns the average of every invoice in the table.HAVINGkeeps only the countries whose own average is strictly higher. - Six decimals. In MySQL,
AVGof adecimal(8,2)column already comes back with 6 decimal places (for example1250.000000). TheROUND(..., 6)does not change the value in MySQL. I keep it so the output looks the same if you test the query somewhere else, such as SQLite, where averages are floating point.
We use HAVING and not WHERE because the condition is on an aggregate. WHERE filters single rows before grouping, so it cannot see a country’s average.
Mistakes that fail the hidden tests
- Copying the broken query that is all over GitHub. A query with
GROUP BY co.country nameandSELECT AVG(total price)(spaces instead of underscores) has been copied into many repos, including my own old one. MySQL rejects it with error 1064. The subquery also has noFROM invoice. Use the query above. - Using
>=instead of>. The task wants countries above the average. In my sample data Brazil’s average is exactly the overall average (800.00). With>=it shows up in the result, with>it does not. - Putting the average check in
WHERE.WHERE AVG(...) > ...is an error. Aggregates go inHAVING. - Comparing with the average of the country averages. The benchmark is the average of all invoices, one number from the
invoicetable. The average of the per-country averages is a different number whenever countries have different invoice counts. - Counting customers instead of invoices.
COUNT(DISTINCT cu.id)gives the number of customers. The question wants the number of invoices, so count invoice rows. - Joining in the wrong direction.
ci.country_id = co.idandcu.city_id = ci.id. If you joincu.id = ci.idby mistake, MySQL will still run the query and give you wrong numbers.
Try it on sample data
I built the tables with these rows and ran the query on MySQL 8.0, MySQL 5.7 and SQLite. The edge cases: Brazil sits exactly on the overall average, Japan has a city but no invoices, and Germany’s average has a long decimal.
country
| id | country_name |
|---|---|
| 1 | India |
| 2 | Germany |
| 3 | Brazil |
| 4 | Japan |
| 5 | France |
city
| id | city_name | country_id |
|---|---|---|
| 1 | Pune | 1 |
| 2 | Delhi | 1 |
| 3 | Berlin | 2 |
| 4 | Sao Paulo | 3 |
| 5 | Osaka | 4 |
| 6 | Lyon | 5 |
customer
| id | customer_name | city_id |
|---|---|---|
| 1 | Asha Traders | 1 |
| 2 | Bharat Foods | 2 |
| 3 | Kraus GmbH | 3 |
| 4 | Rio Mart | 4 |
| 5 | Nila Stores | 1 |
| 6 | Delta Supplies | 3 |
| 7 | Zen Retail | 5 |
| 8 | Lumiere SARL | 6 |
| 9 | Om Electricals | 2 |
invoice
| id | customer_id | total_price |
|---|---|---|
| 1 | 1 | 1200.00 |
| 2 | 1 | 800.00 |
| 3 | 2 | 50.00 |
| 4 | 3 | 1700.00 |
| 5 | 3 | 1300.00 |
| 6 | 4 | 800.00 |
| 7 | 6 | 120.01 |
| 8 | 2 | 129.99 |
| 9 | 8 | 1100.00 |
| 10 | 9 | 200.00 |
| 11 | 8 | 1400.00 |
The 11 invoices add up to 8800.00, so the overall average is 800.00. Per country: India 475.998 (5 invoices), Germany 1040.003333 (3), Brazil 800.00 (1), France 1250.00 (2). Japan has no invoices, so the inner joins drop it.
Expected output
| country_name | invoice_count | average_invoice |
|---|---|---|
| Germany | 3 | 1040.003333 |
| France | 2 | 1250.000000 |
This is the MySQL 8.0 output. MySQL 5.7 printed the same two rows with France first, because 5.7 sorts GROUP BY results and 8.0 does not. Same rows, different order, which is why I said above to add an ORDER BY if your version of the question asks for one.