I'll be honest with you.

For the first year of my PM career, I was 100% dependent on my data analyst for every single number.

Want to know why DAU dropped?

Ping the analyst.

Want to know which feature is actually being used?

Ping the analyst.

Want to pull a quick cohort for a stakeholder meeting happening in 2 hours?

You guessed it — ping the analyst, then wait, then follow up, then wait some more.

It was slow, frustrating, and honestly a little embarrassing.

Then I learned SQL.

Not all of it — I'm not a data engineer and I don't want to be. But enough to answer most of my day-to-day product questions myself. And it changed how I work completely.

This guide is exactly what I wish someone had handed me back then. If you read it fully and practice the queries, you'll go from "I need to ask someone for this data" to "let me pull this myself in 5 minutes."

Let's get into it.

Do PMs Actually Need SQL?

Short answer is yes, but not in the way most people think.

You don't need to build databases. You don't need to write optimized queries that run in milliseconds. You don't need to know what an index is (well, not right now anyway).

What you DO need is to be able to ask questions of your product data and get answers without waiting for someone else. That's it.

Here's what changes when you can write basic SQL:

  • You stop being blocked by analyst availability
  • Your stakeholder conversations get sharper because your numbers are fresh, not 3 days old
  • You catch bugs and anomalies faster because you can spot them yourself
  • You write better specs because you actually understand how data flows in your product
  • You ask smarter questions in data reviews — because you've already dug through the data yourself

I've seen PMs who know SQL get promoted faster, not because SQL itself is a superpower, but because the data fluency that comes with it makes every other part of your job better.

How Data is Stored (Without the Jargon)

Before you write a single query, you need to understand where the data lives.

Think of a database like a collection of spreadsheets that are all connected to each other. Each spreadsheet is called a table. Each table has rows (individual records) and columns (attributes of that record).

For example, imagine you're a PM at a food delivery app. Your database probably has:

  • users table — one row per user, with columns like user_idemailcitycreated_at
  • An orders table — one row per order, with columns like order_iduser_idamountstatuscreated_at
  • restaurants table — one row per restaurant, with columns like restaurant_idnamecitycuisine_type

These tables talk to each other through keys. The user_id in the orders table connects back to the user_id in the users table. This is what lets you ask questions like "which users placed more than 3 orders last month?"

The most important thing you can do as a PM: Get your company's data schema (basically a map of all tables and how they connect) from your data engineer or analyst on Day 1. Save it somewhere you can always find it. It'll save you hours every week.

Where Do You Write SQL?

You don't need to install anything complicated. Most companies use tools that let you write SQL right in a browser. The most common ones you'll run into are:

Tool What It Is Best For
Metabase Browser-based BI tool Quick queries + dashboards
Mode Analytics SQL + visualization tool Analysis + sharing reports
Redash Open-source query tool Teams with custom setups
Google BigQuery Google's cloud data warehouse Large-scale product data
Superset Open-source BI by Apache Self-hosted teams
DBeaver Desktop SQL client Direct DB connections
Looker / Looker Studio Enterprise BI Dashboards + SQL queries

My advice: go to your data or engineering team, tell them you want read-only access to run queries on your product database. Most teams will set this up within a day. Read-only means you can look at data but you can't accidentally change or delete anything — so there's zero risk to the product.

Reading Data: SELECT

This is where everything starts. The SELECT statement is how you tell the database "show me this data."

sql SELECT * FROM users;

This says: give me all columns (*) from the users table. In practice, pulling all columns is rarely useful. Be specific:

sql SELECT user_id, email, created_at
FROM users;

This pulls only the columns you care about — much faster and easier to read.

Real PM use case: You want to see a list of recent signups before a weekly review meeting.

sql SELECT user_id, email, created_at
FROM users
ORDER BY created_at DESC
LIMIT 50;

That's it. You just pulled the 50 most recent signups, sorted newest first. Simple, right?

Filtering Data: WHERE

SELECT gives you all the data. WHERE narrows it down to what you actually care about.

sql SELECT user_id, email, created_at
FROM users
WHERE city = 'Mumbai';

This returns only users from Mumbai.

You can stack conditions using AND and OR:

sql SELECT user_id, email, created_at
FROM users
WHERE city = 'Mumbai'
AND created_at >= '2024-01-01';

This gives you Mumbai users who signed up after Jan 1, 2024.

Date filters are something you'll use every single day as a PM. Here are the most useful variations:

sql-- Users who signed up in the last 30 days
WHERE created_at >= NOW() - INTERVAL '30 days'

-- Users who signed up in a specific month
WHERE DATE_TRUNC('month', created_at) = '2024-03-01'

-- Users who have NOT been active since a date
WHERE last_active_at < '2024-01-01'

Real PM use case: Find users who completed onboarding but never made their first purchase.

sql SELECT user_id
FROM users
WHERE onboarding_completed = true
AND first_purchase_at IS NULL;

That query alone can kick off a whole retention initiative.

6. Sorting and Limiting Results

Two simple but important tools:

ORDER BY sorts your results:

sql SELECT user_id, total_spend
FROM users
ORDER BY total_spend DESC; -- highest to lowest

LIMIT caps how many rows come back:

sql SELECT user_id, total_spend
FROM users
ORDER BY total_spend DESC
LIMIT 10;

Always use LIMIT when you're exploring data. Without it, a query on a large table can return millions of rows and either crash your tool or take forever to load.

Think of LIMIT as a safety net. Get into the habit of writing it at the end of every exploratory query.

Aggregations — Where the Real PM Power Lives

This is where SQL goes from "useful" to "genuinely changes how you work."

Aggregations let you summarize data — count things, sum them up, calculate averages. Combined with GROUP BY, you can slice any metric by any dimension.

The core aggregation functions:

Function What It Does Example
COUNT() Counts rows How many users signed up?
SUM() Adds values Total revenue this month
AVG() Averages values Average session length
MIN() Smallest value Earliest signup date
MAX() Largest value Highest order value
COUNT(DISTINCT) Counts unique values Unique active users (DAU)

Real PM query — Daily Active Users:

SELECT
DATE(event_timestamp) AS date,
COUNT(DISTINCT user_id) AS dau
FROM user_events
WHERE event_timestamp >= NOW() - INTERVAL '30 days'
GROUP BY DATE(event_timestamp)
ORDER BY date;

This gives you a 30-day DAU table in one query. You can drop it straight into a chart.

GROUP BY is what makes aggregations actually useful. It says "give me this number, broken down by this dimension."

SELECT
country,
COUNT(DISTINCT user_id) AS total_users,
AVG(session_length_seconds) AS avg_session
FROM users
GROUP BY country
ORDER BY total_users DESC;

Now you know where your users are and how long they stay in your product, broken down by country. That's a real insight for a growth conversation.

HAVING is like WHERE but for aggregated results:

SELECT
feature_name,
COUNT(DISTINCT user_id) AS users_who_used_it
FROM feature_usage
GROUP BY feature_name
HAVING COUNT(DISTINCT user_id) < 100;

This tells you which features have fewer than 100 users — perfect for a feature audit or sunsetting conversation.

JOINs — Connecting Tables Together

JOINs are the thing most PMs are scared of. They look complicated but once you understand the logic, they're not bad at all.

A JOIN combines two tables based on a shared column. Think of it like a VLOOKUP in Excel, but more powerful.

The two you'll use 90% of the time:

INNER JOIN — Returns only rows that have a match in BOTH tables:

SELECT users.user_id, users.email, orders.order_id, orders.amount
FROM users
INNER JOIN orders ON users.user_id = orders.user_id;

This gives you only users who have placed at least one order.

LEFT JOIN — Returns ALL rows from the left table, plus matching rows from the right (nulls where there's no match):

SELECT users.user_id, users.email, orders.order_id
FROM users
LEFT JOIN orders ON users.user_id = orders.user_id;

This gives you ALL users, including those with no orders. Users with no orders will show NULL in the order_id column.

Real PM use case — Identify users who signed up but never ordered:

SELECT users.user_id, users.email
FROM users
LEFT JOIN orders ON users.user_id = orders.user_id
WHERE orders.order_id IS NULL;

You just found your activation problem segment. That's the list you'd hand to your growth or CRM team for a targeted re-engagement campaign.

Subqueries and CTEs (When You're Ready to Level Up)

Once you're comfortable with JOINs, these two concepts will make your queries cleaner and more powerful.

A subquery is a query inside another query:

SELECT user_id, email
FROM users
WHERE user_id IN (
SELECT DISTINCT user_id
FROM orders
WHERE created_at >= NOW() - INTERVAL '7 days'
);

This finds users who ordered in the last 7 days. The inner query finds the user IDs; the outer query fetches their details.

A CTE (Common Table Expression) does the same thing but is much easier to read. You define it at the top using WITH:

WITH recent_buyers AS (
SELECT DISTINCT user_id
FROM orders
WHERE created_at >= NOW() - INTERVAL '7 days'
)
SELECT users.user_id, users.email
FROM users
INNER JOIN recent_buyers ON users.user_id = recent_buyers.user_id;

Same result, but way more readable — especially when you're sharing your queries with analysts or engineers.

I'd recommend CTEs over subqueries almost every time. Your future self (and your teammates) will thank you.

SQL Cheat Sheet: 10 Queries Every PM Should Bookmark

This is the most practical section of this whole article. Save it, bookmark it, come back to it whenever you need it. Adapt the table and column names to match your own product's schema.

1. Monthly Active Users (MAU)

SELECT
DATE_TRUNC('month', event_timestamp) AS month,
COUNT(DISTINCT user_id) AS mau
FROM user_events
GROUP BY 1
ORDER BY 1;

2. Day 1 / Day 7 / Day 30 Retention

SELECT
DATE(u.created_at) AS signup_date,
COUNT(DISTINCT u.user_id) AS total_signups,
COUNT(DISTINCT CASE WHEN e.event_timestamp BETWEEN u.created_at + INTERVAL '1 day'
AND u.created_at + INTERVAL '2 days' THEN u.user_id END) AS day1_retained,
COUNT(DISTINCT CASE WHEN e.event_timestamp BETWEEN u.created_at + INTERVAL '7 days'
AND u.created_at + INTERVAL '8 days' THEN u.user_id END) AS day7_retained
FROM users u
LEFT JOIN user_events e ON u.user_id = e.user_id
GROUP BY 1
ORDER BY 1;

3. Feature Adoption Rate

SELECT
COUNT(DISTINCT user_id) AS users_who_used_feature,
(COUNT(DISTINCT user_id) * 100.0 / (SELECT COUNT(*) FROM users)) AS adoption_rate_pct
FROM feature_usage
WHERE feature_name = 'your_feature_name'
AND created_at >= '2024-01-01';

4. Funnel Drop-off Analysis

SELECT
COUNT(DISTINCT CASE WHEN step = 'signup' THEN user_id END) AS step1_signup,
COUNT(DISTINCT CASE WHEN step = 'onboarding' THEN user_id END) AS step2_onboarding,
COUNT(DISTINCT CASE WHEN step = 'first_action' THEN user_id END) AS step3_first_action,
COUNT(DISTINCT CASE WHEN step = 'purchase' THEN user_id END) AS step4_purchase
FROM funnel_events
WHERE created_at >= NOW() - INTERVAL '30 days';

5. Churn Signal — Users Inactive for 30 Days

SELECT user_id, email, last_active_at
FROM users
WHERE last_active_at < NOW() - INTERVAL '30 days'
AND account_status = 'active'
ORDER BY last_active_at ASC;

6. Top 10 Power Users

SELECT
user_id,
COUNT(*) AS total_events
FROM user_events
WHERE event_timestamp >= NOW() - INTERVAL '30 days'
GROUP BY user_id
ORDER BY total_events DESC
LIMIT 10;

7. A/B Test Conversion Comparison

SELECT
experiment_group,
COUNT(DISTINCT user_id) AS total_users,
COUNT(DISTINCT CASE WHEN converted = true THEN user_id END) AS conversions,
ROUND(COUNT(DISTINCT CASE WHEN converted = true THEN user_id END) * 100.0
/ COUNT(DISTINCT user_id), 2) AS conversion_rate_pct
FROM ab_test_results
WHERE experiment_name = 'your_experiment_name'
GROUP BY experiment_group;

8. Revenue by Plan / Segment

SELECT
plan_type,
COUNT(DISTINCT user_id) AS subscribers,
SUM(mrr) AS total_mrr,
AVG(mrr) AS avg_mrr
FROM subscriptions
WHERE status = 'active'
GROUP BY plan_type
ORDER BY total_mrr DESC;

9. Zero-Usage Features (Feature Audit)

SELECT f.feature_name
FROM features f
LEFT JOIN feature_usage fu ON f.feature_name = fu.feature_name
AND fu.created_at >= NOW() - INTERVAL '90 days'
WHERE fu.feature_name IS NULL;

10. NPS Segment Behavior

SELECT
CASE
WHEN nps_score >= 9 THEN 'Promoter'
WHEN nps_score >= 7 THEN 'Passive'
ELSE 'Detractor'
END AS nps_segment,
COUNT(DISTINCT u.user_id) AS users,
AVG(total_sessions) AS avg_sessions,
AVG(total_spend) AS avg_spend
FROM nps_responses n
JOIN users u ON n.user_id = u.user_id
GROUP BY 1
ORDER BY 2 DESC;

SQL Best Practices for PMs

A few things I've learned the hard way:

  • Always use LIMIT when exploring. On large tables, a query without LIMIT can timeout or return millions of rows. Start with LIMIT 100 and expand once you know the query works.
  • Always get read-only access. Never run SQL on production with write permissions. You could accidentally delete data. Read-only access protects you and the product.
  • Comment your queries. Use -- to add notes, especially when you save queries for later. "Future you" will have no memory of what you were trying to do.
  • Validate your output. Cross-check your query result against a known number (like a dashboard metric) before sharing it in a meeting.
  • Save your queries. Keep a personal query library in Notion, GitHub Gist, or even a Google Doc. You'll reuse 80% of them with minor changes.
  • SQL helps you collaborate better, not replace your analyst. When you come to your data analyst with a half-written query and a clear question, you'll get a much better answer much faster than if you just say "can you pull our retention numbers?"
  • Use AI. any Ai model can help you write any sql query you want with simple prompts saving a lot of time.

Your 4-Week SQL Learning Plan

Don't try to learn all of this in a weekend. Here's a plan that actually works:

Week Focus What to Practice
Week 1 SELECT, WHERE, ORDER BY, LIMIT Pull user lists, filter by date, sort results
Week 2 COUNT, SUM, AVG, GROUP BY, HAVING Calculate DAU, MAU, revenue by segment
Week 3 INNER JOIN, LEFT JOIN Combine user + event + order tables
Week 4 Subqueries, CTEs, real product data Build funnel queries, retention queries on your actual DB

Free resources I'd recommend:

  • SQLZoo — beginner, browser-based practice
  • Mode SQL Tutorial — built for analysts, great for PMs
  • GoPractice SQL for PMs — specifically designed for product managers
  • LeetCode Easy SQL problems — once you're comfortable with the basics

Wrapping Up

SQL didn't make me a data scientist. It made me a better product manager.

The biggest shift wasn't the technical skill itself — it was the mindset shift that came with it. When you know you can get the answer yourself in 10 minutes, you ask more questions. You validate your assumptions more often. You catch things earlier. You show up to meetings with better context.

If you take nothing else from this guide, take this: you don't need to know everything about SQL to get value from it. Learn SELECTWHEREGROUP BY, and JOIN well, and you'll be able to answer 70% of your daily product questions on your own.

Start small. Pull one query from the cheat sheet above, adapt it to your product's table names, and run it this week. That's it.

I write about what I'm actually learning and doing as a product manager over at anukulsaini.com. If this was useful, subscribe — I publish stuff like this regularly.