Advanced SQL: Aggregates, Subqueries & Window Functions
Master GROUP BY aggregates, subqueries, CTEs, and window functions to answer complex analytical questions from your data.
Learning Objectives
- → Use GROUP BY with aggregate functions: COUNT, SUM, AVG, MIN, MAX
- → Filter groups with HAVING
- → Write correlated and non-correlated subqueries
- → Use CTEs (WITH) for readable complex queries
- → Apply window functions: ROW_NUMBER, RANK, LAG, LEAD
Aggregate Functions
SELECT
COUNT(*) AS total_scans,
COUNT(DISTINCT host) AS unique_hosts,
MIN(created_at) AS first_scan,
MAX(created_at) AS last_scan
FROM scans;
| Function | What it does |
|---|---|
| COUNT(*) | Count all rows |
| COUNT(col) | Count non-NULL values |
| SUM(col) | Sum of values |
| AVG(col) | Average |
| MIN(col) | Minimum value |
| MAX(col) | Maximum value |
| GROUP_CONCAT(col) | Comma-join strings (SQLite) |
GROUP BY
-- Scan count per user
SELECT
u.username,
COUNT(s.id) AS scan_count,
COUNT(DISTINCT s.host) AS unique_hosts
FROM users u
LEFT JOIN scans s ON s.user_id = u.id
GROUP BY u.id, u.username
ORDER BY scan_count DESC;
-- Open ports per service
SELECT
service,
COUNT(*) AS count,
AVG(port) AS avg_port
FROM ports
WHERE status = 'open'
GROUP BY service;
HAVING (filter groups)
-- Users with more than 5 scans
SELECT user_id, COUNT(*) AS cnt
FROM scans
GROUP BY user_id
HAVING cnt > 5;
-- Services with more than 3 open ports
SELECT service, COUNT(*) AS open_count
FROM ports
WHERE status = 'open'
GROUP BY service
HAVING open_count > 3
ORDER BY open_count DESC;
Key distinction:
- WHERE filters individual rows (before grouping)
- HAVING filters groups (after aggregation)
LEFT JOIN for "none" queries
-- Users who have NEVER done a scan
SELECT u.username
FROM users u
LEFT JOIN scans s ON s.user_id = u.id
WHERE s.id IS NULL;
-- Hosts with no open ports
SELECT h.ip
FROM hosts h
LEFT JOIN ports p ON p.host_id = h.id AND p.status = 'open'
WHERE p.id IS NULL;
Subqueries
-- Non-correlated: inner query runs once
SELECT * FROM users
WHERE id IN (
SELECT DISTINCT user_id FROM scans WHERE status = 'complete'
);
-- Correlated: inner query references outer
SELECT u.username
FROM users u
WHERE EXISTS (
SELECT 1 FROM scans s
WHERE s.user_id = u.id
AND s.created_at > DATE('now', '-7 days')
);
-- Scalar subquery
SELECT
username,
(SELECT COUNT(*) FROM scans s WHERE s.user_id = u.id) AS scan_count
FROM users u;
CTEs (Common Table Expressions)
-- WITH makes complex queries readable
WITH recent_scans AS (
SELECT *
FROM scans
WHERE created_at > DATE('now', '-30 days')
),
user_stats AS (
SELECT
user_id,
COUNT(*) AS scan_count,
COUNT(DISTINCT host) AS unique_hosts
FROM recent_scans
GROUP BY user_id
)
SELECT
u.username,
us.scan_count,
us.unique_hosts
FROM users u
JOIN user_stats us ON us.user_id = u.id
WHERE us.scan_count > 2
ORDER BY us.scan_count DESC;
Window Functions
Window functions compute a value across a set of rows related to the current row:
-- ROW_NUMBER: rank within partition
SELECT
username,
scan_count,
ROW_NUMBER() OVER (ORDER BY scan_count DESC) AS rank
FROM user_stats;
-- RANK: same rank for ties, gaps after
SELECT
host,
open_ports,
RANK() OVER (ORDER BY open_ports DESC) AS danger_rank
FROM host_stats;
-- Running total
SELECT
created_at,
scan_count,
SUM(scan_count) OVER (ORDER BY created_at) AS running_total
FROM daily_scans;
-- LAG/LEAD: access previous/next row
SELECT
created_at,
scan_count,
LAG(scan_count, 1) OVER (ORDER BY created_at) AS prev_day,
scan_count - LAG(scan_count, 1) OVER (ORDER BY created_at) AS delta
FROM daily_scans;
CASE Expression
-- Conditional column
SELECT
host,
status,
CASE
WHEN status = 'open' THEN 'VULNERABLE'
WHEN status = 'filtered' THEN 'UNKNOWN'
ELSE 'SAFE'
END AS risk_level
FROM ports;
-- CASE in aggregate
SELECT
COUNT(CASE WHEN status = 'open' THEN 1 END) AS open_ports,
COUNT(CASE WHEN status = 'closed' THEN 1 END) AS closed_ports
FROM ports;
String & Date Functions
-- String
SELECT UPPER(username), LOWER(email), LENGTH(username) FROM users;
SELECT SUBSTR(email, 1, INSTR(email, '@') - 1) AS local_part FROM users;
SELECT REPLACE(host, '10.', '192.168.') FROM scans;
-- Date (SQLite)
SELECT DATE('now'); -- 2025-01-15
SELECT DATE('now', '-7 days'); -- 7 days ago
SELECT STRFTIME('%Y-%m', created_at) -- month bucket
FROM scans;
-- Group by month
SELECT
STRFTIME('%Y-%m', created_at) AS month,
COUNT(*) AS scans
FROM scans
GROUP BY month
ORDER BY month;
Using your scan database: (1) count scans per status, (2) find users with more than 2 scans in the last 7 days using HAVING, (3) find users with zero scans using LEFT JOIN + IS NULL, (4) compute the average number of open ports per host.
What is the difference between WHERE and HAVING?
What does COUNT(DISTINCT col) count?
Write a CTE that: (1) computes monthly scan counts, (2) uses LAG() to get the previous month's count, (3) calculates month-over-month change. Output: month, scan_count, prev_month, change.
What does LAG(col, 1) return?
Write an EXISTS subquery to find users who ran a scan on a host that was also scanned by at least one other user. Use correlated subqueries.
What does EXISTS return?