Author: Jaime M. Shaker
Email: jaime.m.shaker@gmail.com
Website: https://www.shaker.dev
LinkedIn: https://www.linkedin.com/in/jaime-shaker/
This project is an opportunity to practice your SQL skills. This project was inspired by the Braintree Analytics Code Challenge and was created to strengthen/sharpen your SQL knowledge.
❗ If you find this repository helpful, please consider giving it a ⭐. Thanks! ❗
Please Note:
This project is meant to measure/gauge/practice your technical abilities with SQL.
- All work should be done in SQL.
- Do not alter the CSV files.
- Most, if not all of these questions are intended purely as a measure of your SQL skills and not as actual questions that one would expect to answer from an employer.
An SQL Code Challenge Walkthrough has been created if you would like to follow along in a step by step fashion.
The questions are listed below with the expected results and answers hidden so you can cross reference your answers.
This repository contains all of the necessary files, datasets and directories for running a PostgresSQL server in a Docker Container. The only prerequisite is that you should already have Docker Desktop installed and running on your computer.
https://www.docker.com/products/docker-desktop/
- This project uses the latest version of
PostgreSQL
, but any SQL database may be used. - Directories and Files details:
README.md
: This is the file your are currently reading..gitignore
: The files and directories listed in this file will be ignored by Git/GitHub.docker-compose.yaml
: With this yaml (Yet Another Markup Language) file you can define the services (Postgres/MySQL/Python...) and with a single command, you can spin everything up or tear it all down.source_data/csv_data
: This is the location of the CSV files needed to copy into our database.db
: In the current directory, create an empty directory named 'db
'. Although this directory is ignored by Git/GitHub, PostgreSQL requires a directory to keep data persistent if you are running a Docker container. There will be no need to alter any of the files that reside there once the container starts running.images
: This is the location of the image files displayed in this repository.walkthrough/
: A directory with a beginner friendly walkthrough of all of the steps I took to complete the SQL challenge.source_data/
: This directory contains all the files and scripts within our Docker container.source_data/csv_data
: This is the location of the CSV files needed to copy/insert into our database.source_data/scripts/
: This directory will contain all sql scripts I used to complete the challenge.source_data/csv_output/
: This directory will contain all CSV files generated by some of our queries.
❕ ⭐ Start here if you want to follow along with the SQL Challenge WALKTHROUGH ⭐ ❕
1. Create Database, Tables and Relations.
Using the CSV files located in source_data/csv_data
, create your new SQL database and tables with the properly formatted data.
- Add a numeric, auto-incrementing Primary Key to every table.
- In the
countries
table, add the columncreated_on
with the current date. - Create a one-to-one and one-to-many relationship with the countries table as the parent table.
2. List Regions and Country Count
List all of the regions and the total number of countries in each region. Order by country count in descending order and capitalize the region name.
Click to expand expected results!
region | country_count |
---|---|
Africa | 59 |
Americas | 57 |
Asia | 50 |
Europe | 48 |
Oceania | 26 |
Antartica | 1 |
Click to expand answer!
SELECT
-- initcap() capitalizes the first letter of every word in a string.
initcap(region) AS region,
count(*) AS country_count
FROM
cleaned_data.countries
GROUP BY
-- Aggregate functions 'count()' require you to group all column fields.
region
ORDER BY
country_count DESC;
3. List Sub-Regions and City Count
List all of the sub-regions and the total number of cities in each sub-region. Order by sub-region name alphabetically.
Click to expand expected results!
sub_region | city_count |
---|---|
Australia And New Zealand | 329 |
Central Asia | 560 |
Eastern Asia | 3164 |
Eastern Europe | 2959 |
Latin America And The Caribbean | 7204 |
Melanesia | 60 |
Micronesia | 15 |
Northern Africa | 1152 |
Northern America | 5844 |
Northern Europe | 2025 |
Polynesia | 22 |
Southeastern Asia | 2627 |
Southern Asia | 6848 |
Southern Europe | 3238 |
Subsaharan Africa | 3223 |
Western Asia | 1400 |
Western Europe | 3952 |
Click to expand answer!
SELECT
initcap(t1.sub_region) AS sub_region,
count(*) AS city_count
FROM
cleaned_data.countries AS t1
JOIN
cleaned_data.cities AS t2
ON
t1.country_code_2 = t2.country_code_2
GROUP BY
t1.sub_region
ORDER BY
t1.sub_region;
4. Specific Sub-Region and String Functions
List all of the countries and the total number of cities in the Northern Europe sub-region. List the country names in uppercase and order the list by the length of the country name and alphabetically in ascending order.
Click to expand expected results!
country_name | city_count |
---|---|
JERSEY | 1 |
LATVIA | 39 |
NORWAY | 127 |
SWEDEN | 148 |
DENMARK | 75 |
ESTONIA | 20 |
FINLAND | 142 |
ICELAND | 12 |
IRELAND | 64 |
LITHUANIA | 61 |
ISLE OF MAN | 2 |
FAROE ISLANDS | 29 |
UNITED KINGDOM | 1305 |
Click to expand answer!
SELECT
-- upper() returns your string in uppercase.
upper(t1.country_name) AS country_name,
count(*) AS city_count
FROM
cleaned_data.countries AS t1
JOIN
cleaned_data.cities AS t2
ON
t1.country_code_2 = t2.country_code_2
WHERE
t1.sub_region = 'northern europe'
GROUP BY
t1.country_name
ORDER BY
-- length() returns the number or characters in a string including spaces.
length(t1.country_name), t1.country_name;
5. List Specific Countries by Year
List all of the countries and the total number of cities in the Southern Europe sub-region that were inserted in 2021. Capitalize the country names and order alphabetically by the LAST letter of the country name and the number of cities.
Click to expand expected results!
country_name | city_count |
---|---|
Andorra | 5 |
Albania | 11 |
Bosnia And Herzegovina | 15 |
Croatia | 22 |
North Macedonia | 28 |
Malta | 32 |
Serbia | 58 |
Slovenia | 74 |
Greece | 64 |
Portugal | 109 |
Spain | 302 |
San Marino | 2 |
Montenegro | 12 |
Italy | 542 |
Click to expand answer!
SELECT
initcap(t1.country_name) AS country_name,
count(*) AS city_count
FROM
cleaned_data.countries AS t1
JOIN
cleaned_data.cities AS t2
ON
t1.country_code_2 = t2.country_code_2
WHERE
t1.sub_region = 'southern europe'
AND
-- extract() & date_part() functions allow you to breakdown dates and timestamps to individual years, month, days, hours.....
EXTRACT('year' FROM t2.insert_date) = 2021
GROUP BY
t1.country_name
ORDER BY
-- substring() function returns a specific portion of a string.
substring(t1.country_name,length(t1.country_name),1), city_count;
6. List Anti-Join
List all of the countries in the region of Asia that did NOT have a city with an inserted date from June 2021 through Sept 2021.
Click to expand expected results!
country_name |
---|
Brunei Darussalam |
Kuwait |
Macao |
Singapore |
Click to expand answer!
SELECT
-- Distinct will only return unique values
DISTINCT initcap(t1.country_name) AS country_name
FROM
cleaned_data.countries AS t1
-- Left join will return all matching values from the left table (cleaned_data.countries) and
-- only the matching values from the right table (cleaned_tables.cities) resulting in NULLS where
-- there are no matches in the left table.
LEFT JOIN
cleaned_data.cities AS t2
ON
t1.country_code_2 = t2.country_code_2
AND
t2.insert_date BETWEEN '2021-06-01' AND '2021-10-01'
WHERE
t1.region = 'asia'
-- Only return values that did NOT have a match with the countries table.
AND
t2.country_code_2 IS NULL;
7. Reversable Names
List the country, city name, population and city name length for the city names that are palindromes in the Western Asia sub-region. Format the population with a thousands separator (1,000) and format the length of the city name in roman numerals. Order by the length of the city name in descending order and alphabetically in ascending order.
Click to expand expected results!
country_name | city_name | population | roman_numeral_length |
---|---|---|---|
Yemen | Hajjah | 46,568 | VI |
Syrian Arab Republic | Hamah | 696,863 | V |
Turkey | Kavak | 21,692 | V |
Turkey | Kinik | 29,803 | V |
Turkey | Tut | 10,161 | III |
Click to expand answer!
SELECT
initcap(t1.country_name) AS country_name,
initcap(t2.city_name) AS city_name,
-- to_char() takes non-string data types and returns them as strings.
to_char(t2.population, '999G999') AS population,
to_char(length(t2.city_name), 'RN') AS roman_numeral_length
FROM
cleaned_data.countries AS t1
JOIN
cleaned_data.cities AS t2
ON
t1.country_code_2 = t2.country_code_2
WHERE
t2.city_name = reverse(t2.city_name)
AND
t1.sub_region = 'western asia'
ORDER BY
length(t2.city_name) DESC, t2.city_name ASC;
8. Search with Wildcard and Case
List all of the countries that end in 'stan'. Make your query case-insensitive and list whether the total population of the cities listed is an odd or even number for cities inserted in 2022. Order by whether the population value is odd or even in ascending order and country name in alphabetical order.
Click to expand expected results!
country_name | total_population | odd_or_even |
---|---|---|
Afghanistan | 6,006,530 | Even |
Kazakhstan | 4,298,264 | Even |
Kyrgyzstan | 1,017,644 | Even |
Pakistan | 26,344,480 | Even |
Tajikistan | 2,720,953 | Odd |
Turkmenistan | 419,607 | Odd |
Uzbekistan | 3,035,547 | Odd |
Click to expand answer!
SELECT
initcap(t1.country_name) AS country_name,
to_char(sum(t2.population), '99G999G999') total_population,
CASE
WHEN (sum(t2.population) % 2) = 0
THEN 'Even'
ELSE
'Odd'
END AS odd_or_even
FROM
cleaned_data.countries AS t1
JOIN
cleaned_data.cities AS t2
ON
t1.country_code_2 = t2.country_code_2
WHERE
t1.country_name ILIKE '%stan'
AND
EXTRACT('year' FROM t2.insert_date) = 2022
GROUP BY
t1.country_name
ORDER BY
odd_or_even, country_name;
9. Ranking Regions
List the third most populated city ranked by region WITHOUT using limit or offset. List the region name, city name, population and order the results by region.
Click to expand expected results!
region | city_name | third_largest_pop |
---|---|---|
Africa | Kinshasa | 12,836,000 |
Americas | New York | 18,972,871 |
Asia | Delhi | 32,226,000 |
Europe | Paris | 11,060,000 |
Oceania | Brisbane | 2,360,241 |
Click to expand answer!
WITH get_city_rank_cte AS (
SELECT
t1.region,
t2.city_name,
t2.population AS third_largest_pop,
DENSE_RANK() OVER (PARTITION BY t1.region ORDER BY t2.population DESC) AS rnk
FROM
cleaned_data.countries AS t1
JOIN
cleaned_data.cities AS t2
ON
t1.country_code_2 = t2.country_code_2
WHERE
t2.population IS NOT NULL
GROUP BY
t1.region,
t2.city_name,
t2.population
)
SELECT
initcap(region) AS region,
initcap(city_name) AS city_name,
to_char(third_largest_pop, '99G999G999') AS third_largest_pop
FROM
get_city_rank_cte
WHERE
rnk = 3;
10. Using Buckets
List the bottom third of all countries in the Western Asia sub-region that speak Arabic. Include the row number and country name. Order by row number.
Click to expand expected results!
row_id | country_name |
---|---|
9 | saudi arabia |
10 | syrian arab republic |
11 | united arab emirates |
12 | yemen |
Click to expand answer!
WITH get_ntile_cte AS (
SELECT
ROW_NUMBER() OVER (ORDER BY t1.country_name) AS rn,
t1.country_name,
-- ntile() window functions returns groups of data section into 'buckets'.
NTILE(3) OVER (ORDER BY t1.country_name) AS nt
FROM
cleaned_data.countries AS t1
JOIN
cleaned_data.languages AS t2
ON
t1.country_code_2 = t2.country_code_2
WHERE
t1.sub_region = 'western asia'
AND
t2.language = 'arabic'
)
SELECT
rn AS row_id,
country_name
FROM
get_ntile_cte
WHERE
nt = 3;
11. Using Arrays
Create a query that lists country name, capital name, population, languages spoken and currency name for countries in the Northen Africa sub-region. There can be multiple currency names and languages spoken per country. Add multiple values for the same field into an array.
Click to expand expected results!
country_name | city_name | population | languages | currencies |
---|---|---|---|---|
algeria | algiers | 3415811 | {french,arabic,kabyle} | algerian dinar |
egypt | cairo | 20296000 | {arabic} | egyptian pound |
libya | tripoli | 1293016 | {arabic} | libyan dinar |
morocco | rabat | 572717 | {arabic,tachelhit,moroccan tamazight,french} | moroccan dirham |
sudan | khartoum | 7869000 | {arabic,english} | sudanese pound |
tunisia | tunis | 1056247 | {french,arabic} | tunisian dinar |
Click to expand answer!
WITH get_row_values AS (
SELECT
t1.country_name,
t2.city_name,
t2.population,
-- array_agg() aggregates multiple values and returns them in 'array' format.
array_agg(t3.LANGUAGE) AS languages,
t4.currency_name AS currencies
FROM
cleaned_data.countries AS t1
JOIN
cleaned_data.cities AS t2
ON
t1.country_code_2 = t2.country_code_2
JOIN
cleaned_data.languages AS t3
ON
t1.country_code_2 = t3.country_code_2
JOIN
cleaned_data.currencies AS t4
ON
t1.country_code_2 = t4.country_code_2
WHERE
t1.sub_region = 'northern africa'
AND
t2.capital = TRUE
GROUP BY
t1.country_name,
t2.city_name,
t2.population,
t4.currency_name
)
SELECT
*
FROM
get_row_values;
12. Using Case and Percentages
Produce a query that returns the city names for cities in the U.S. that were inserted on April, 28th 2022. List how many vowels and consonants are present in the city name and concatnate their percentage to the their respective count in parenthesis.
Click to expand expected results!
city_name | vowel_count_perc | consonants_count_perc |
---|---|---|
standish | 2 (25.00%) | 6 (75%) |
grand forks | 2 (18.18%) | 9 (81.82%) |
camano | 3 (50.00%) | 3 (50%) |
cedar hills | 3 (27.27%) | 8 (72.73%) |
gladstone | 3 (33.33%) | 6 (66.67%) |
whitehall | 3 (33.33%) | 6 (66.67%) |
homewood | 4 (50.00%) | 4 (50%) |
willowbrook | 4 (36.36%) | 7 (63.64%) |
port salerno | 4 (33.33%) | 8 (66.67%) |
vadnais heights | 5 (33.33%) | 10 (66.67%) |
jeffersonville | 5 (35.71%) | 9 (64.29%) |
Click to expand answer!
WITH get_letter_count AS (
SELECT
t1.city_name,
length(t1.city_name) string_length,
-- regexp_replace() returns a vlue that has been manipulated by a regular expression.
length(regexp_replace(t1.city_name, '[aeiou]', '', 'gi')) AS consonant_count
FROM
cleaned_data.cities AS t1
WHERE
t1.insert_date = '2022-04-28'
AND
t1.country_code_2 in ('us')
),
get_letter_diff AS (
SELECT
city_name,
string_length - consonant_count AS vowels,
round(100 * (string_length - consonant_count) / string_length::NUMERIC, 2) AS vowel_perc,
consonant_count AS consonants,
round( 100 * (consonant_count)::NUMERIC / string_length, 2)::float AS consonants_perc
FROM
get_letter_count
)
SELECT
city_name,
vowels || ' (' || vowel_perc || '%)' AS vowel_count_perc,
consonants || ' (' || consonants_perc || '%)' AS consonants_count_perc
FROM
get_letter_diff
ORDER BY
vowels;
13. Most Consecutive Days
List the most consecutive inserted dates and the capitalized city names for cities in Canada that where inserted in April 2022.
Click to expand expected results!
most_consecutive_dates | city_name |
---|---|
2022-04-22 | South Dundas |
2022-04-23 | La Prairie |
2022-04-24 | Elliot Lake |
2022-04-25 | Lachute |
Click to expand answer!
DROP TABLE IF EXISTS get_dates;
CREATE TEMP TABLE get_dates AS (
SELECT
DISTINCT ON (insert_date) insert_date AS insert_date,
city_name
FROM
cleaned_data.cities
WHERE
country_code_2 = 'ca'
AND
insert_date BETWEEN '2022-04-01' AND '2022-04-30'
ORDER BY
insert_date
);
DROP TABLE IF EXISTS get_diff;
CREATE TEMP TABLE get_diff AS (
SELECT
city_name,
insert_date,
EXTRACT('day' FROM insert_date) - ROW_NUMBER() OVER (ORDER BY insert_date) AS diff
FROM
get_dates
);
DROP TABLE IF EXISTS get_diff_count;
CREATE TEMP TABLE get_diff_count AS (
SELECT
city_name,
insert_date,
count(*) OVER (PARTITION BY diff) AS diff_count
FROM
get_diff
);
WITH get_rank AS (
SELECT
DENSE_RANK() OVER (ORDER BY diff_count desc) AS rnk,
insert_date,
city_name
FROM
get_diff_count
)
SELECT
insert_date AS most_consecutive_dates,
initcap(city_name) AS city_name
FROM
get_rank
WHERE
rnk = 1
ORDER BY
insert_date;
14. Month over Month in View
Create a view that lists the month-year, the number of cities inserted for that month, a running city count total and the month over month percentage growth for 2021.
Format the cities count and the running total with the thousands separator and format the month over month growth with a plus symbol and percentage symbol
Example:
month_year | cities_inserted | running_total | month_over_month |
---|---|---|---|
Feb-2021 | 1,291 | 2,762 | +87.76% |
Click to expand expected results!
month_year | cities_inserted | running_total | month_over_month |
---|---|---|---|
Jan-2021 | 1,471 | 1,471 | |
Feb-2021 | 1,291 | 2,762 | +87.76% |
Mar-2021 | 1,485 | 4,247 | +53.77% |
Apr-2021 | 1,508 | 5,755 | +35.51% |
May-2021 | 1,509 | 7,264 | +26.22% |
Jun-2021 | 1,395 | 8,659 | +19.20% |
Jul-2021 | 1,394 | 10,053 | +16.10% |
Aug-2021 | 1,481 | 11,534 | +14.73% |
Sep-2021 | 1,452 | 12,986 | +12.59% |
Oct-2021 | 1,446 | 14,432 | +11.14% |
Nov-2021 | 1,378 | 15,810 | +9.55% |
Dec-2021 | 1,472 | 17,282 | +9.31% |
Click to expand answer!
DROP VIEW IF EXISTS cleaned_data.view_2021_growth;
CREATE VIEW cleaned_data.view_2021_growth AS (
WITH get_month_count AS (
SELECT
date_trunc('month', insert_date) as single_month,
count(*) AS monthly_count
FROM
cleaned_data.cities
WHERE
EXTRACT('year' FROM insert_date) = 2021
GROUP BY
single_month
ORDER BY
single_month
),
get_running_total AS (
SELECT
single_month::date,
monthly_count,
sum(monthly_count) OVER (ORDER BY single_month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS total_num_cities
FROM
get_month_count
),
get_month_over_month AS (
SELECT
single_month,
monthly_count,
total_num_cities,
round(100.0 * ((total_num_cities - Lag(total_num_cities, 1) OVER (ORDER BY single_month)) / Lag(total_num_cities, 1) OVER (ORDER BY single_month))::NUMERIC, 2) AS month_over_month
FROM
get_running_total
)
SELECT
to_char(single_month, 'Mon-YYYY') AS month_year,
to_char(monthly_count, '9G999') AS cities_inserted,
to_char(total_num_cities, '99G999') AS running_total,
to_char(month_over_month, 'sg99.99') || '%' AS month_over_month
FROM
get_month_over_month
);
SELECT
*
FROM
cleaned_data.view_2021_growth;
15. Stored Procedure to CSV
Create and call a stored procedure that lists a unique row id number, insert date, country name, city name, population and languages spoken for countries in the Latin America and Caribbean sub-region that were inserted on either '2022-04-09', '2022-04-28' or '2022-08-11'.
Order by the insert date and output the results (including headers) to a CSV file located in /source_data/csv_output/ .
Click to expand expected results!
Results located in /source_data/csv_output/output.csv
Click to expand answer!
CREATE OR REPLACE PROCEDURE cleaned_data.sproc_output ()
LANGUAGE plpgsql
AS
$sproc$
BEGIN
COPY (
SELECT
ROW_NUMBER() OVER (ORDER BY t1.insert_date) AS row_id,
t1.insert_date,
t2.country_name,
t1.city_name,
t1.population,
array_agg(t3.language) AS languages
FROM
cleaned_data.cities AS t1
JOIN
cleaned_data.countries AS t2
ON
t1.country_code_2 = t2.country_code_2
LEFT JOIN
cleaned_data.languages AS t3
ON
t2.country_code_2 = t3.country_code_2
WHERE
t2.sub_region = 'latin america and the caribbean'
AND
t1.insert_date IN ('2022-04-09', '2022-04-28', '2022-08-11')
GROUP BY
t1.insert_date,
t2.country_name,
t1.city_name,
t1.population
ORDER BY
t1.insert_date
)
TO '/var/lib/postgresql/source_data/csv_output/output.csv' DELIMITER ',' CSV HEADER;
END
$sproc$;
-- Call the stored procedure
CALL cleaned_data.sproc_output();
❕ Start here if you want to follow along with the SQL Challenge WALKTHROUGH ❕
❗ If you found this repository helpful, please consider giving it a ⭐. Thanks! ❗