Cheat sheet
pandas cheat sheet
pandas cheat sheet grouped by task: filter rows, groupby, merge, missing values, text cleaning and dates. Every snippet runs on a real store dataset.
Cheat sheet · 12 tasks · 47 snippets, each run on DuckDB
The queries analysts write every week, grouped by the job you need done. Every snippet runs as written on the Harbor & Pine store tables, and each group links to the level that teaches it.
Dialect: DuckDB, which follows PostgreSQL (coming from SQL Server? see the table at the end). Tables: customers, products, orders, order_items, returns, employees, web_sessions, marketing_spend. Line revenue is quantity × unit_price × (1 − discount_pct / 100).
SELECT *
FROM products
LIMIT 10; SELECT name, category, unit_price
FROM products; SELECT name,
unit_price - unit_cost AS margin,
ROUND((unit_price - unit_cost) / unit_price * 100, 1) AS margin_pct
FROM products; Any arithmetic with NULL gives NULL: products with no unit_cost get a NULL margin.
SELECT DISTINCT category, subcategory
FROM products
ORDER BY category, subcategory; SELECT order_id, order_date, channel
FROM orders
WHERE status = 'refunded'; SELECT name, category, unit_price
FROM products
WHERE category IN ('Decor', 'Kitchen')
AND (unit_price < 50 OR NOT is_active); AND binds tighter than OR. Bracket every OR you mix with AND.
SELECT order_id, order_date
FROM orders
WHERE order_date >= DATE '2024-03-01'
AND order_date < DATE '2024-04-01'; BETWEEN '2024-03-01' AND '2024-03-31' silently drops orders placed after midnight on the 31st.
SELECT customer_id, email
FROM customers
WHERE email ILIKE '%@gmail.com'; ILIKE ignores case (PostgreSQL, DuckDB). Elsewhere: LOWER(email) LIKE ...
SELECT customer_id, email
FROM customers
WHERE referral_source IS NULL; = NULL is never true. Use IS NULL / IS NOT NULL.
SELECT COALESCE(referral_source, 'unknown') AS source,
COUNT(*) AS customers
FROM customers
GROUP BY 1; SELECT COUNT(*) AS customers,
COUNT(referral_source) AS with_source
FROM customers; SELECT channel,
SUM(amount) / NULLIF(COUNT(*), 0) AS avg_daily_spend
FROM marketing_spend
GROUP BY channel; SELECT name, unit_cost
FROM products
ORDER BY unit_cost NULLS FIRST; SELECT product_id, name, unit_price
FROM products
ORDER BY unit_price DESC, product_id; SELECT order_id, order_date
FROM orders
ORDER BY order_date DESC
LIMIT 10; SELECT product_id, name
FROM products
ORDER BY product_id
LIMIT 10 OFFSET 10; SELECT COUNT(*) AS lines,
SUM(quantity) AS units,
AVG(unit_price) AS avg_price,
MIN(unit_price), MAX(unit_price)
FROM order_items; SELECT channel, status, COUNT(*) AS orders
FROM orders
GROUP BY channel, status
ORDER BY channel, orders DESC; SELECT channel, COUNT(DISTINCT customer_id) AS buyers
FROM orders
GROUP BY channel; SELECT customer_id, COUNT(*) AS orders
FROM orders
WHERE status = 'completed'
GROUP BY customer_id
HAVING COUNT(*) >= 20; Run order: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. WHERE filters rows, HAVING filters groups.
SELECT CASE WHEN unit_price < 50 THEN 'under 50'
WHEN unit_price < 200 THEN '50-199'
ELSE '200+' END AS band,
COUNT(*) AS products
FROM products
GROUP BY 1; CASE stops at the first true WHEN, so order the tests from narrow to wide.
SELECT channel,
COUNT(*) FILTER (WHERE status = 'cancelled') AS cancelled,
ROUND(AVG(CASE WHEN status = 'cancelled' THEN 1.0 ELSE 0 END) * 100, 1) AS cancel_pct
FROM orders
GROUP BY channel; FILTER works in PostgreSQL and DuckDB; SUM(CASE WHEN ... THEN 1 ELSE 0 END) works everywhere.
SELECT p.category,
ROUND(SUM(oi.quantity * oi.unit_price * (1 - oi.discount_pct / 100)), 2) AS revenue
FROM order_items AS oi
JOIN products AS p ON p.product_id = oi.product_id
GROUP BY p.category
ORDER BY revenue DESC; SELECT c.customer_id, c.email
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL; SELECT c.customer_id, COUNT(o.order_id) AS app_orders
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id AND o.channel = 'app'
WHERE c.segment = 'business'
GROUP BY c.customer_id; Put o.channel = 'app' in WHERE and the customers with no app orders disappear: the LEFT JOIN becomes an inner join.
SELECT o.channel,
COUNT(*) AS lines,
COUNT(DISTINCT o.order_id) AS orders
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
GROUP BY o.channel; After joining order lines, each order appears once per line (fan-out). Never SUM an order-level column like shipping_fee here.
SELECT e.first_name || ' ' || e.last_name AS employee,
m.first_name || ' ' || m.last_name AS manager
FROM employees AS e
LEFT JOIN employees AS m ON m.employee_id = e.manager_id; SELECT DATE_TRUNC('month', order_date) AS month,
COUNT(*) AS orders
FROM orders
WHERE order_date >= DATE '2025-01-01'
GROUP BY 1
ORDER BY 1; SELECT EXTRACT(year FROM order_date) AS yr,
EXTRACT(month FROM order_date) AS mo,
EXTRACT(dow FROM order_date) AS weekday, -- 0 = Sunday
COUNT(*) AS orders
FROM orders
GROUP BY 1, 2, 3; SELECT r.return_id,
r.returned_on - CAST(o.order_date AS DATE) AS days_to_return
FROM returns AS r
JOIN order_items AS oi ON oi.order_item_id = r.order_item_id
JOIN orders AS o ON o.order_id = oi.order_id; DATE minus DATE gives whole days in PostgreSQL and DuckDB. SQL Server and BigQuery use DATEDIFF / DATE_DIFF.
SELECT COUNT(*) AS orders
FROM orders
WHERE order_date >= TIMESTAMP '2025-12-31' - INTERVAL '30 days'; SELECT UPPER(TRIM(state)) AS state, COUNT(*) AS customers
FROM customers
GROUP BY 1
ORDER BY customers DESC; SELECT SPLIT_PART(email, '@', 2) AS domain, COUNT(*) AS customers
FROM customers
GROUP BY 1
ORDER BY customers DESC; SELECT LOWER(TRIM(email)) AS email_key,
COUNT(*) AS records,
MIN(customer_id) AS keep_id
FROM customers
GROUP BY 1
HAVING COUNT(*) > 1; SELECT first_name || ' ' || last_name AS full_name
FROM employees; CONCAT(a, b) also works and treats NULL as empty; || returns NULL if either side is NULL.
SELECT name, unit_price
FROM products
WHERE unit_price > (SELECT AVG(unit_price) FROM products); SELECT c.customer_id, c.email
FROM customers AS c
WHERE EXISTS (
SELECT 1
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
JOIN returns AS r ON r.order_item_id = oi.order_item_id
WHERE o.customer_id = c.customer_id
); NOT IN (subquery) returns no rows at all if the subquery holds a NULL. NOT EXISTS is safe.
WITH order_totals AS (
SELECT order_id,
SUM(quantity * unit_price * (1 - discount_pct / 100)) AS merch
FROM order_items
GROUP BY order_id
)
SELECT o.customer_id,
COUNT(*) AS orders,
ROUND(SUM(t.merch), 2) AS lifetime_value,
ROUND(AVG(t.merch), 2) AS avg_order_value
FROM orders AS o
JOIN order_totals AS t ON t.order_id = o.order_id
WHERE o.status = 'completed'
GROUP BY o.customer_id; WITH merch AS (
SELECT order_id, SUM(quantity * unit_price) AS merch
FROM order_items
GROUP BY order_id
)
SELECT o.channel,
SUM(o.shipping_fee) AS shipping,
ROUND(SUM(m.merch), 2) AS merchandise
FROM orders AS o
LEFT JOIN merch AS m ON m.order_id = o.order_id
GROUP BY o.channel; WITH ranked AS (
SELECT category, name, unit_price,
ROW_NUMBER() OVER (PARTITION BY category
ORDER BY unit_price DESC, product_id) AS rn
FROM products
)
SELECT category, name, unit_price
FROM ranked
WHERE rn <= 3; You cannot filter on a window function in WHERE: wrap it in a CTE or subquery first.
SELECT name, unit_price,
ROW_NUMBER() OVER (ORDER BY unit_price DESC) AS row_num,
RANK() OVER (ORDER BY unit_price DESC) AS rnk, -- 1, 2, 2, 4
DENSE_RANK() OVER (ORDER BY unit_price DESC) AS dense_rnk -- 1, 2, 2, 3
FROM products; WITH daily AS (
SELECT CAST(order_date AS DATE) AS day, SUM(shipping_fee) AS fees
FROM orders
WHERE order_date >= DATE '2025-01-01'
GROUP BY 1
)
SELECT day, fees,
SUM(fees) OVER (ORDER BY day) AS fees_ytd
FROM daily; WITH monthly AS (
SELECT DATE_TRUNC('month', order_date) AS month, COUNT(*) AS orders
FROM orders
GROUP BY 1
)
SELECT month, orders,
LAG(orders) OVER (ORDER BY month) AS prev,
ROUND((orders - LAG(orders) OVER (ORDER BY month)) * 100.0
/ LAG(orders) OVER (ORDER BY month), 1) AS mom_pct
FROM monthly; WITH daily AS (
SELECT CAST(order_date AS DATE) AS day, COUNT(*) AS orders
FROM orders
GROUP BY 1
)
SELECT day, orders,
AVG(orders) OVER (ORDER BY day
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS avg_7d
FROM daily; ROWS counts rows, not days: a day with no orders has no row, so fill the calendar first if gaps matter.
SELECT ROUND(SUM(oi.quantity * oi.unit_price * (1 - oi.discount_pct / 100))
/ COUNT(DISTINCT o.order_id), 2) AS aov
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status = 'completed'; SELECT source,
COUNT(*) AS sessions,
ROUND(AVG(CASE WHEN converted THEN 1.0 ELSE 0 END) * 100, 2) AS conversion_pct
FROM web_sessions
GROUP BY source
ORDER BY conversion_pct DESC; SELECT p.category,
ROUND(COUNT(r.return_id) * 100.0 / COUNT(*), 1) AS return_pct
FROM order_items AS oi
JOIN products AS p ON p.product_id = oi.product_id
LEFT JOIN returns AS r ON r.order_item_id = oi.order_item_id
GROUP BY p.category; Most courses and many offices use Microsoft SQL Server. The ideas are identical; these are the spellings that differ. Everything in the right column runs on this site.
| Task | SQL Server | This site (DuckDB) |
|---|---|---|
| First 10 rows | SELECT TOP 10 * FROM orders | SELECT * FROM orders LIMIT 10 |
| Today's date | CAST(GETDATE() AS date) | CURRENT_DATE |
| Replace NULL | ISNULL(referral_source, 'unknown') | COALESCE(referral_source, 'unknown') |
| Add 30 days | DATEADD(day, 30, signup_date) | signup_date + INTERVAL '30 days' |
| Days between dates | DATEDIFF(day, signup_date, GETDATE()) | DATE_DIFF('day', signup_date, CURRENT_DATE) |
| First day of the month | DATEFROMPARTS(YEAR(d), MONTH(d), 1) | DATE_TRUNC('month', signup_date) |
| Year of a date | YEAR(signup_date) | YEAR(signup_date) or EXTRACT(year FROM signup_date) |
| Join text | first_name + ' ' + last_name | first_name || ' ' || last_name |
| Text length | LEN(city) -- ignores trailing spaces | LENGTH(city) -- counts them |
| Change a type | CONVERT(int, '42') or CAST('42' AS int) | CAST('42' AS INTEGER) or '42'::INTEGER |
| 7 / 2 on whole numbers | 3 (integer division) | 3.5; write 7 // 2 for 3 |
| Match text ignoring case | LIKE '%gmail%' (case-insensitive by default) | ILIKE '%gmail%' (LIKE is case-sensitive) |
| True / false column | bit: 1 / 0 | BOOLEAN: true / false |
https://analystquest.com/resources/sql-cheat-sheet/ · Free levels to practise every snippet: analystquest.com
Cheat sheet
pandas cheat sheet grouped by task: filter rows, groupby, merge, missing values, text cleaning and dates. Every snippet runs on a real store dataset.
Cheat sheet
Excel formulas for analysts, grouped by task: SUMIFS, COUNTIFS, XLOOKUP, IF, text cleaning, dates and duplicates, with the shortcuts that save the most time.
Interview prep
Questions from real analyst interviews, each with a model answer, the answer that loses points and a level to practise it. Terms you are unsure of are in the glossary.