agentsclimarketplace

Sql queries

Skill Sidsaladi9/persona-os/plugins/product-manager-os/skills/sql-queries

Persona OS by The Product Channel — drop-in operating systems for Claude Code. Product Manager OS: 40 book-grounded, battle-tested skills + commands + memory + getprompts/getskills.

Install
npx -y skills add Sidsaladi9/persona-os --skill sql-queries

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Translates a product question into clean, correct SQL against a described schema and explains it line by line. Covers the core product-analytics patterns — funnels, retention and cohorts, DAU/MAU stickiness, and event counts — plus how to sanity-check the output. Use when you say "write me a query for", "how many users did X then Y", "what's our 7-day retention", "DAU/MAU by week", "build a funnel from signup to activation", or "is this number right?"

SKILL.md

12.8 KB, ~3.3k tokens by cl100k_base, as published. Nobody here has run it

SQL Queries

Turn a product question into trustworthy SQL using a small set of battle-tested product-analytics patterns. Most product questions reduce to one of four shapes — funnel, retention/cohort, active-user ratio, or event count — and getting the shape right matters more than clever syntax.

Grounded in: Lean Analytics — Croll & Yoskovitz: answer the product question (funnels, retention windows, DAU/MAU) — correctly, not approximately. Go deeper (The Product Channel): Product Metrics

When to use this

  • A PM or analyst asks "how many users went from signup to first key action, and where do they drop off?"
  • You need 1/7/30-day retention or a weekly cohort triangle and want it correct, not approximately correct.
  • You want DAU, WAU, MAU, or the DAU/MAU stickiness ratio over time.
  • You need to count events or unique users per feature, segment, or time bucket — and trust the number.
  • A dashboard figure looks off and you want a query plus a sanity-check protocol to confirm or debunk it.

Before you start (gather these)

  • The question, stated as a metric — e.g. "7-day retention of users who signed up in May," not "how's retention."
  • The schema — table names, key columns, and grain. At minimum: the events/activity table, its user-id column, its timestamp column, and how an "event type" is identified.
  • The SQL dialect — Postgres, BigQuery, Snowflake, Redshift, MySQL, DuckDB. Date functions differ; this matters.
  • Definitions — what counts as "active," "activated," "retained"? What's the time grain (day/week/month) and timezone?

If two or more of these are missing or vague, ASK 2–4 sharp questions before writing SQL (e.g. "Which table holds events and what's the user-id column?", "Postgres or BigQuery?", "Is retention day-N exact, or N-day-window?", "What event defines activation?"). If the schema and question are already provided, proceed and state every assumption inline as a -- comment in the query.

Process

  1. Classify the shape. Decide which of the four patterns fits: funnel, retention/cohort, active-user ratio, or event count. Say which one and why. Most questions are exactly one; some compose two.
  2. Pin the grain and the definitions. One row per what? (user, user-day, cohort-week.) Write the definition of the metric in one sentence before any SQL.
  3. Anchor the population. Define the cohort/denominator first — the set of users in scope and their anchor date (signup, first event). Retention and funnels live or die on a correct denominator.
  4. Handle time deliberately. Truncate to the grain (date_trunc), be explicit about timezone (e.g. DATE_TRUNC('day', event_time AT TIME ZONE 'America/New_York') to bucket by account-local days instead of UTC), and use half-open intervals [start, end) to avoid double-counting boundary rows. 4b. Exclude incomplete cohorts. For any windowed/day-N metric, drop cohorts whose window hasn't fully elapsed — WHERE cohort_start + window <= now() (use the window END for ranges). A cohort from 3 days ago can't have day-7 retention yet; including it silently understates the number.
  5. De-dupe to the grain. Use COUNT(DISTINCT user_id) for people, raw COUNT(*) for events — never confuse the two. Collapse multiple events per user per period before joining.
  6. Write the query with CTEs, not nesting. One named CTE per logical step (cohort → activity → join → aggregate). Readable beats clever.
  7. Sanity-check before you trust it. Run the checks in the template: row counts, denominator size, a spot-check on one known user, and monotonicity (retention and funnels only ever decrease).
  8. Explain it. Two or three sentences: what each CTE does, what one row of output means, and the one assumption most likely to be wrong.

Output template

## Question
[Restate the product question as a precise metric, with grain and timezone.]

## Shape
[Funnel | Retention/Cohort | Active-user ratio | Event count] — [one line: why this shape.]

## Assumptions
- Dialect: [Postgres | BigQuery | Snowflake | …]
- Events table: `[table]`, user id: `[col]`, timestamp: `[col]`, event type: `[col] = '[value]'`
- "[Metric term, e.g. retained]" means: [exact definition].
- Timezone: [UTC | account tz]; grain: [day | week | month].

## SQL
\`\`\`sql
-- PATTERN: RETENTION (day-N exact). Swap pattern block as needed.
-- NOTE: both sides of the day-N comparison MUST be day-truncated. cohort_day is
-- DATE_TRUNC'd and active_day is DATE_TRUNC'd, so they align. If you compare a
-- DATE_TRUNC'd anchor against a RAW timestamp signup column, `= cohort_day + INTERVAL '7 day'`
-- almost never matches (timestamps land at 14:32:07, not midnight) → silent zero retention.
WITH cohort AS (                       -- denominator: who, anchored when
  SELECT
    user_id,
    DATE_TRUNC('day', MIN(event_time)) AS cohort_day
  FROM [events_table]
  GROUP BY user_id
),
activity AS (                          -- one row per user per active day
  SELECT DISTINCT
    user_id,
    DATE_TRUNC('day', event_time) AS active_day
  FROM [events_table]
)
SELECT
  c.cohort_day,
  COUNT(DISTINCT c.user_id)                          AS cohort_size,
  COUNT(DISTINCT CASE
    WHEN a.active_day = c.cohort_day + INTERVAL '7 day'
    THEN c.user_id END)                              AS retained_d7,
  ROUND(100.0 * COUNT(DISTINCT CASE
    WHEN a.active_day = c.cohort_day + INTERVAL '7 day'
    THEN c.user_id END) / NULLIF(COUNT(DISTINCT c.user_id), 0), 1) AS retention_d7_pct
FROM cohort c
LEFT JOIN activity a ON a.user_id = c.user_id
-- Drop incomplete cohorts: a cohort hasn't had time to reach day 7 yet if
-- cohort_day + 7d is still in the future. Including it understates retention.
WHERE c.cohort_day + INTERVAL '7 day' <= DATE_TRUNC('day', now())
GROUP BY c.cohort_day
ORDER BY c.cohort_day;
\`\`\`

**Retention — N-day WINDOW `[+7d, +14d)` (the canonical "week-2 retention"):**
\`\`\`sql
-- "Week-2 retention" = active ANY time in the window [cohort_day + 7d, cohort_day + 14d),
-- NOT active exactly on day 7. Half-open interval avoids double-counting the boundary.
WITH cohort AS (
  SELECT user_id, DATE_TRUNC('day', MIN(event_time)) AS cohort_day
  FROM [events_table]
  GROUP BY user_id
),
activity AS (
  SELECT DISTINCT user_id, DATE_TRUNC('day', event_time) AS active_day
  FROM [events_table]
)
SELECT
  c.cohort_day,
  COUNT(DISTINCT c.user_id)                          AS cohort_size,
  COUNT(DISTINCT CASE
    WHEN a.active_day >= c.cohort_day + INTERVAL '7 day'
     AND a.active_day <  c.cohort_day + INTERVAL '14 day'
    THEN c.user_id END)                              AS retained_w2,
  ROUND(100.0 * COUNT(DISTINCT CASE
    WHEN a.active_day >= c.cohort_day + INTERVAL '7 day'
     AND a.active_day <  c.cohort_day + INTERVAL '14 day'
    THEN c.user_id END) / NULLIF(COUNT(DISTINCT c.user_id), 0), 1) AS retention_w2_pct
FROM cohort c
LEFT JOIN activity a ON a.user_id = c.user_id
-- Exclude recency-truncated cohorts: the FULL window must have elapsed (use the
-- window END, +14d, not +7d) or late-window returns are silently missing.
WHERE c.cohort_day + INTERVAL '14 day' <= DATE_TRUNC('day', now())
GROUP BY c.cohort_day
ORDER BY c.cohort_day;
\`\`\`

### Pattern swaps (drop in place of the SELECT above)

**Funnel (ordered steps, drop-off):**
\`\`\`sql
WITH steps AS (
  SELECT user_id,
    MIN(CASE WHEN event_type = 'signup'     THEN event_time END) AS t1,
    MIN(CASE WHEN event_type = 'activated'  THEN event_time END) AS t2,
    MIN(CASE WHEN event_type = 'purchased'  THEN event_time END) AS t3
  FROM [events_table]
  GROUP BY user_id
)
SELECT
  COUNT(*) FILTER (WHERE t1 IS NOT NULL)                       AS step1_signup,
  COUNT(*) FILTER (WHERE t2 >= t1)                             AS step2_activated,
  COUNT(*) FILTER (WHERE t3 >= t2 AND t2 >= t1)                AS step3_purchased
FROM steps;  -- t2 >= t1 enforces order; add `AND t2 <= t1 + INTERVAL '7 day'` for windowed funnels
\`\`\`

**DAU / MAU stickiness:**
\`\`\`sql
-- One CTE per step, no correlated per-day subquery. The trailing-30 MAU is a
-- COUNT(DISTINCT ...) over a 30-day window frame, so the events table is scanned
-- once instead of re-scanned for every day (the correlated-subquery trap).
WITH user_days AS (                        -- de-dupe to one row per user per active day
  SELECT DISTINCT user_id, DATE_TRUNC('day', event_time) AS day
  FROM [events_table]
),
dau AS (                                   -- distinct users active each day
  SELECT day, COUNT(DISTINCT user_id) AS dau
  FROM user_days
  GROUP BY day
),
mau AS (                                   -- trailing-30 distinct users via window frame
  SELECT day,
    COUNT(DISTINCT user_id) OVER (
      ORDER BY day
      RANGE BETWEEN INTERVAL '29 day' PRECEDING AND CURRENT ROW
    ) AS mau_trailing_30
  FROM user_days
)
SELECT
  d.day,
  d.dau,
  m.mau_trailing_30,
  ROUND(100.0 * d.dau / NULLIF(m.mau_trailing_30, 0), 1) AS stickiness_pct
FROM dau d
JOIN (SELECT DISTINCT day, mau_trailing_30 FROM mau) m USING (day)
ORDER BY d.day;
-- PERF CAVEAT: COUNT(DISTINCT ...) as a window aggregate isn't supported in every
-- engine (e.g. older Postgres). If it errors, fall back to a self-join on user_days
-- over the [day-29, day] range with COUNT(DISTINCT) grouped by day — still one pass
-- per day but no correlated scalar subquery in the SELECT list.
\`\`\`

**Event count (per feature per week):**
\`\`\`sql
SELECT DATE_TRUNC('week', event_time) AS wk, event_type,
       COUNT(*) AS events, COUNT(DISTINCT user_id) AS unique_users
FROM [events_table]
GROUP BY 1, 2 ORDER BY 1, 2;
\`\`\`

## Result
| [cohort_day] | [cohort_size] | [retained_d7] | [retention_d7_pct] |
|---|---|---|---|
| [2026-05-01] | [1,240] | [388] | [31.3] |

## Sanity checks (run before trusting)
- [ ] **Denominator size** is the number you expect (e.g. cohort_size ≈ known signups for the period).
- [ ] **People vs events:** used `COUNT(DISTINCT user_id)` for users, `COUNT(*)` for events — not swapped.
- [ ] **Spot-check one user:** pick a known user_id, verify their row matches raw events by hand.
- [ ] **Monotonicity:** funnel steps and retention curves only ever go *down* — any increase is a bug.
- [ ] **Boundaries:** half-open intervals `[start, end)`; no row counted in two buckets; timezone explicit.
- [ ] **Complete cohorts only:** excluded any cohort where `cohort_start + window > now()` — recency-truncated cohorts haven't had time to mature and drag the metric down.
- [ ] **Anchor alignment:** both sides of a day-N comparison are day-truncated (a raw-timestamp anchor vs a truncated one = silent zero matches).
- [ ] **Nulls & div-by-zero:** `NULLIF(denominator, 0)`; LEFT JOIN didn't silently drop the cohort.

## Read of the result
[2–3 sentences: what one row means, the headline number, and the single assumption most likely to flip the answer.]

Avoid (anti-patterns)

  • Counting events when you mean users (or vice versa) — COUNT(*) where you needed COUNT(DISTINCT user_id) silently inflates every number.
  • INNER JOIN on the cohort — drops users who never returned, turning a retention denominator into only the retained. Use LEFT JOIN and anchor on the cohort table.
  • Off-by-one time windowsBETWEEN is inclusive on both ends and double-counts midnight rows; use half-open >= start AND < end. Forgetting timezone shifts whole cohorts by a day.
  • Day-N exact vs N-day-window confusion — "day-7 retention" (active exactly on day 7) and "week-1 retention" (active any time in days 1–7) give very different numbers. State which.
  • Trusting the first run — shipping a number without the spot-check and monotonicity checks. A query that runs is not a query that's correct.

Tips

  • Build the denominator first and confirm its size against a known figure before adding any retention/funnel logic — most wrong answers are wrong denominators.
  • Prefer CTEs over nested subqueries. One CTE per step reads like the explanation you'll have to write anyway, and is far easier to debug.
  • Window functions earn their keep for "first/next event" logic: ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time) to grab a user's first action beats a self-join every time.
  • State the dialect up front and use its native date functionsDATE_TRUNC (Postgres/Snowflake) vs TIMESTAMP_TRUNC/DATE_TRUNC (BigQuery) vs DATE_FORMAT (MySQL). A query for the wrong dialect is a query that doesn't run.

Gives 0 of the 12 instructions most quality gates skills give in ~3.3k tokens

Counted across 1,195 of the 2,094 authors here whose files we hold, read 2026-08-06

  • read the output and check the exit codein 55 of 1195, across 14 files
  • verify requirements using a line-by-line checklistin 53 of 1195, across 12 files
  • identify the verification command proving the claimin 53 of 1195, across 12 files
  • run the full verification commandin 51 of 1195, across 11 files
  • verify output confirms the claimin 49 of 1195, across 10 files
  • check version control diff after agent delegationin 45 of 1195, across 5 files
  • state claim with evidencein 43 of 1195, across 3 files
  • run the test suitein 32 of 1195, across 24 files
  • keep state in memory by defaultin 27 of 1195, across 6 files
  • make prototype runnable with one commandin 26 of 1195, across 5 files
  • detect the package manager from lockfilesin 24 of 1195, across 5 files
  • produce a verification reportin 23 of 1195, across 12 files

Said here and by no other author read

  • ask clarifying questions if schema or definitions are missing
  • classify the question into one of four analytics shapes
  • define the metric and grain in one sentence before writing sql
  • anchor the cohort denominator first
  • truncate timestamps explicitly to the correct time grain
  • exclude incomplete cohorts from windowed metrics

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.