Skip to content

SQL Analysis — COVID-19 Data Analysis

Setup

Load covid_countries_clean.csv into a table covid. Key columns: location, continent, date, new_cases, new_deaths, total_cases, total_deaths, total_cases_per_million, total_deaths_per_million, population, new_cases_7d, people_vaccinated.


Q1 — Highest Cases Per Capita (Fair Comparison)

-- Latest cumulative cases per million, per country
WITH latest AS (
    SELECT location, MAX(date) AS max_date
    FROM covid
    WHERE continent IS NOT NULL
    GROUP BY location
)
SELECT
    c.location,
    ROUND(c.total_cases_per_million, 0)     AS cases_per_million,
    ROUND(c.total_deaths_per_million, 0)    AS deaths_per_million,
    ROUND(c.population / 1000000, 1)        AS population_millions
FROM covid c
JOIN latest l ON c.location = l.location AND c.date = l.max_date
ORDER BY cases_per_million DESC
LIMIT 15;

Why per-capita changes everything

By raw total cases, large countries dominate. By cases per million, small countries with intense outbreaks rise to the top. This is the fair comparison.


Q2 — Case Fatality Rate by Country

WITH latest AS (
    SELECT location, MAX(date) AS max_date
    FROM covid WHERE continent IS NOT NULL
    GROUP BY location
)
SELECT
    c.location,
    c.total_cases,
    c.total_deaths,
    ROUND(c.total_deaths * 100.0 / NULLIF(c.total_cases, 0), 2) AS cfr_pct
FROM covid c
JOIN latest l ON c.location = l.location AND c.date = l.max_date
WHERE c.total_cases > 100000   -- meaningful sample
ORDER BY cfr_pct DESC
LIMIT 15;

CFR is not the true death rate

Case Fatality Rate = deaths / confirmed cases. Countries with limited testing detect fewer mild cases, inflating their CFR. CFR reflects testing capacity as much as disease severity — always caveat this.


Q3 — Global Monthly Case Totals (Sum Daily, Not Cumulative)

SELECT
    year_month,
    ROUND(SUM(new_cases), 0)        AS monthly_new_cases,
    ROUND(SUM(new_deaths), 0)       AS monthly_new_deaths
FROM covid
WHERE location = 'World'
GROUP BY year_month
ORDER BY year_month;

Sum new_cases, never total_cases

total_cases is cumulative — summing it across days is meaningless. To get a monthly total, sum the daily new_cases.


Q4 — Detect Each Country's Peak Day

WITH ranked AS (
    SELECT
        location,
        date,
        new_cases_7d,
        ROW_NUMBER() OVER (PARTITION BY location ORDER BY new_cases_7d DESC) AS rn
    FROM covid
    WHERE continent IS NOT NULL
)
SELECT
    location,
    date            AS peak_date,
    ROUND(new_cases_7d, 0) AS peak_daily_cases_7d_avg
FROM ranked
WHERE rn = 1
ORDER BY peak_daily_cases_7d_avg DESC
LIMIT 15;

Uses the 7-day average (not raw daily) to find the true peak, ignoring single-day reporting spikes.


Q5 — Vaccination Progress

WITH latest AS (
    SELECT location, MAX(date) AS max_date
    FROM covid WHERE continent IS NOT NULL AND people_vaccinated IS NOT NULL
    GROUP BY location
)
SELECT
    c.location,
    ROUND(c.people_vaccinated / c.population * 100, 1) AS pct_vaccinated_1dose,
    ROUND(c.population / 1000000, 1)                   AS pop_millions
FROM covid c
JOIN latest l ON c.location = l.location AND c.date = l.max_date
WHERE c.population > 1000000
ORDER BY pct_vaccinated_1dose DESC
LIMIT 15;

Q6 — Wave Identification (Month-over-Month Change)

WITH monthly AS (
    SELECT
        location,
        year_month,
        SUM(new_cases) AS monthly_cases
    FROM covid
    WHERE location IN ('United States', 'United Kingdom', 'India')
    GROUP BY location, year_month
)
SELECT
    location,
    year_month,
    ROUND(monthly_cases, 0)                                     AS cases,
    ROUND(LAG(monthly_cases) OVER (PARTITION BY location ORDER BY year_month), 0) AS prev_month,
    CASE
        WHEN monthly_cases > LAG(monthly_cases) OVER (PARTITION BY location ORDER BY year_month) * 1.5
        THEN 'WAVE RISING'
        WHEN monthly_cases < LAG(monthly_cases) OVER (PARTITION BY location ORDER BY year_month) * 0.5
        THEN 'declining'
        ELSE 'stable'
    END                                                         AS phase
FROM monthly
ORDER BY location, year_month;

Q7 — Continent-Level Summary

WITH latest AS (
    SELECT location, MAX(date) AS max_date
    FROM covid WHERE continent IS NOT NULL
    GROUP BY location
)
SELECT
    c.continent,
    COUNT(DISTINCT c.location)              AS countries,
    ROUND(SUM(c.total_cases) / 1000000, 1)  AS total_cases_millions,
    ROUND(SUM(c.total_deaths) / 1000, 0)    AS total_deaths_thousands,
    ROUND(SUM(c.total_deaths) * 100.0 / NULLIF(SUM(c.total_cases), 0), 2) AS cfr_pct
FROM covid c
JOIN latest l ON c.location = l.location AND c.date = l.max_date
GROUP BY c.continent
ORDER BY total_cases_millions DESC;

← Data Cleaning · Next: Dashboard Design →