Competitive Analysis — Learn LAG/LEAD, RANK, PERCENT_RANK, and Scoring in Practice
AdvancedCompetitive AnalysisLAG / LEADRANK / DENSE_RANKCASE WHEN PivotPERCENT_RANKPostgreSQL Compatible5 questions
QUESTION 6
LAG() + CTE — Visualize Quarterly Competitor Trend Acceleration and Slowdown with Period-over-Period Growth
LAG / LEADCTEPeriod-over-Period GrowthNULLIF · Zero-Division Protection
Background

LAG(expr, offset, default) is a window function that returns the value offset rows before the current row. It is an essential pattern for period-over-period and year-over-year calculations.

LAG(revenue) OVER (
  PARTITION BY company_name   -- Do not take the preceding row across companies
  ORDER BY     quarter        -- Define the “one row earlier” position by quarter
)                              -- Default offset=1, default=NULL
Prevent division by zero with NULLIF: if the previous period is 0, revenue / prev_revenue causes a division-by-zero error. NULLIF(prev_revenue, 0) returns NULL when the value is 0, so division by NULL produces NULL (not an error) and is handled safely.
Calculate LAG only once in a CTE: writing LAG directly multiple times in the SELECT for a growth-rate calculation reduces maintainability. First establish prev_revenue in a CTE and reuse it in the outer query as a best practice.
Problem

Using quarterly revenue from three companies, calculate the previous-period revenue (prev_revenue) and period-over-period growth rate (growth_rate_pct, rounded to one decimal place). Return company_name, quarter, revenue, prev_revenue, growth_rate_pct, sorted by company_name ascending and then quarter ascending. Q1 has NULL for both prev_revenue and growth_rate_pct.

Tables
▸ quarterly_revenue
company_namequarterrevenue
AlphaTech2023Q1300
AlphaTech2023Q2360
AlphaTech2023Q3280
AlphaTech2023Q4400
BetaSoft2023Q1250
BetaSoft2023Q2220
BetaSoft2023Q3290
BetaSoft2023Q4310
GammaSys2023Q1120
GammaSys2023Q2150
GammaSys2023Q3170
GammaSys2023Q4160

※ Revenue unit: JPY 100 million

Expected Output

Expected output (company_name ascending → quarter ascending):

company_namequarterrevenueprev_revenuegrowth_rate_pct
AlphaTech2023Q1300NULLNULL
AlphaTech2023Q236030020.0
AlphaTech2023Q3280360-22.2
AlphaTech2023Q440028042.9
BetaSoft2023Q1250NULLNULL
BetaSoft2023Q2220250-12.0
BetaSoft2023Q329022031.8
BetaSoft2023Q43102906.9
GammaSys2023Q1120NULLNULL
GammaSys2023Q215012025.0
GammaSys2023Q317015013.3
GammaSys2023Q4160170-5.9

AlphaTech Q3 (-22.2%) is the notable drop, followed by a sharp recovery in Q4 (+42.9%). BetaSoft’s Q2 (-12.0%) is another negative-growth warning.

QUESTION 7
RANK() + CTE — Category-Level Competitor Ranking and Top-N Filtering
RANK / DENSE_RANKCTE + WHERECategory Top-NHandling Tied Ranks
Background

RANK(), DENSE_RANK(), and ROW_NUMBER() are all window functions that return ranks, but they handle ties differently.

FunctionWhen two rows tie for secondNext rank
ROW_NUMBER()2, 3 (pseudo-unique assignment)4
RANK()2, 2 (tie)4 (skips a number)
DENSE_RANK()2, 2 (tie)3 (consecutive)
The CTE + WHERE top-N extraction pattern: a window-function result cannot be referenced in WHERE in the same SELECT (WHERE comes earlier in the logical evaluation order). Assign the rank in a CTE and filter it in the outer WHERE as the standard pattern.
WITH ranked AS (
  SELECT *, RANK() OVER (PARTITION BY category ORDER BY annual_revenue DESC) AS rnk
  FROM product_revenue
)
SELECT * FROM ranked WHERE rnk <= 2;  -- Top rank 2 or better in each category, including ties
Problem

From the competitor revenue data for each category, assign the within-category sales rank (rank_in_category) and return only companies ranked 2 or better in each category, including ties. Return category, company_name, annual_revenue, rank_in_category, sorted by category ascending and then rank_in_category ascending.

Tables
▸ product_revenue
company_namecategoryannual_revenue
AlphaTechCloud4200
AlphaTechSecurity1800
AlphaTechAI/ML950
BetaSoftCloud2800
BetaSoftAI/ML1500
BetaSoftData Analytics800
GammaSysSecurity900
GammaSysAI/ML650
GammaSysData Analytics420
DeltaNetCloud1200
DeltaNetData Analytics600

※ annual_revenue unit: JPY 100 million

Expected Output

Expected output (category ascending → rank_in_category ascending):

categorycompany_nameannual_revenuerank_in_category
AI/MLBetaSoft15001
AI/MLAlphaTech9502
CloudAlphaTech42001
CloudBetaSoft28002
SecurityAlphaTech18001
SecurityGammaSys9002
Data AnalyticsBetaSoft8001
Data AnalyticsDeltaNet6002

AlphaTech ranks first in Cloud and Security. BetaSoft ranks first in AI/ML and Data Analytics, showing clear category specialization.

QUESTION 8
Multiple-CTE Scoring — Automatically Calculate Overall KPI Grades with Staged Aggregation
Multiple WITH CTEsCASE WHENCompetitor ScoringStaged Aggregation and Grading
Background

Multiple CTEs can be defined as comma-separated entries in a WITH clause, and a preceding CTE can be referenced by a later CTE. This decomposes complex aggregation into named, readable stages.

WITH
step1 AS (
  SELECT ..., CASE WHEN col >= 30 THEN 3 ELSE 1 END AS score
  FROM source_table
),
step2 AS (          -- step1 can be referenced here
  SELECT *, score_a + score_b AS total
  FROM step1
)
SELECT * FROM step2;
Be careful when omitting ELSE from CASE WHEN: omitting ELSE in CASE WHEN ... END means ELSE NULL. In scoring logic, always specify ELSE the minimum score.
Problem

From KPI data for six competitors, score revenue scale, growth rate, and NPS from 1 to 3 points, then calculate the total (maximum 9 points) and overall grade (S/A/B/C). Return company_name, revenue_score, growth_score, nps_pts, total_score, grade, sorted by total_score descending and then company_name ascending.

Scoring rules: revenue_score (revenue_bn ≥30→3, ≥10→2, else 1) | growth_score (growth_pct ≥25→3, ≥10→2, else 1) | nps_pts (nps_score ≥65→3, ≥50→2, else 1) | grade (total ≥8→S, ≥6→A, ≥4→B, else C)

Tables
▸ competitor_kpis
company_namerevenue_bngrowth_pctnps_score
AlphaTech42.018.572
BetaSoft28.012.358
GammaSys15.031.265
DeltaNet5.08.148
EpsilonSys3.0-2.441
ZetaCloud2.05.655

※ revenue_bn: revenue (billions of JPY) / growth_pct: year-over-year growth (%) / nps_score: customer NPS

Expected Output

Expected output (total_score descending → company_name ascending):

company_namerevenue_scoregrowth_scorenps_ptstotal_scoregrade
AlphaTech3238S
GammaSys2338S
BetaSoft3227A
ZetaCloud1124B
DeltaNet1113C
EpsilonSys1113C

GammaSys has only mid-sized revenue, but its 31.2% growth and NPS 65 give it the same top S grade as AlphaTech. BetaSoft has large revenue, but average growth and NPS leave it at A.

QUESTION 9
CASE WHEN Pivot — Convert Quarterly Revenue from Long to Wide and Calculate Within-Year Growth
SUM + CASE WHENGROUP BYLong-to-Wide TransformationConditional Aggregation and NULLIF
Background

Conditional aggregation uses the SUM(CASE WHEN ... END) pattern to convert long-format (row-oriented) data into wide-format (column-oriented) data. PostgreSQL has no dedicated PIVOT syntax, so this is the standard implementation.

SELECT
  company_name,
  SUM(CASE WHEN quarter = 'Q1' THEN revenue ELSE 0 END) AS q1_rev,
  SUM(CASE WHEN quarter = 'Q2' THEN revenue ELSE 0 END) AS q2_rev
FROM   quarterly_sales
GROUP BY company_name;
ELSE 0 vs ELSE NULL: ELSE 0 adds 0 for non-matching rows. With ELSE NULL, SUM ignores NULL, so the result is the same, but AVG and COUNT behave differently. ELSE 0 is the conventional choice for pivot aggregation.
Problem

Using quarterly revenue for three companies in 2024, pivot Q1–Q4 revenue into columns, then calculate the annual total (annual_total) and the within-year Q1→Q4 growth rate (q4_vs_q1_pct, rounded to one decimal place). Return company_name, q1_rev, q2_rev, q3_rev, q4_rev, annual_total, q4_vs_q1_pct, sorted by annual_total descending.

Tables
▸ quarterly_sales
company_nameyearquarterrevenue
AlphaTech2024Q1340
AlphaTech2024Q2410
AlphaTech2024Q3320
AlphaTech2024Q4450
BetaSoft2024Q1260
BetaSoft2024Q2240
BetaSoft2024Q3310
BetaSoft2024Q4340
GammaSys2024Q1130
GammaSys2024Q2165
GammaSys2024Q3185
GammaSys2024Q4170

※ Revenue unit: JPY 100 million / the table is assumed to contain multiple years, so filter with WHERE year = 2024

Expected Output

Expected output (annual_total descending):

company_nameq1_revq2_revq3_revq4_revannual_totalq4_vs_q1_pct
AlphaTech340410320450152032.4
BetaSoft260240310340115030.8
GammaSys13016518517065030.8

All three companies grow by roughly 30% from Q1 to Q4. AlphaTech has a temporary Q3 dip (320) but reaches +32.4% for the year. GammaSys has a small Q4 pullback but still grows +30.8% for the year.

QUESTION 10
PERCENT_RANK() + LAG() + Two-Stage CTE — Track Annual Changes in Competitor Relative Position
PERCENT_RANKLAG + Two-Stage CTERelative-Position ChangePercentile Tracking
Background

PERCENT_RANK() returns each row’s relative rank as a percentile from 0.0 to 1.0. The formula is (rank - 1) / (total_rows - 1).

FormulaExample with 5 rows (ORDER BY revenue ASC)
(rank-1) / (5-1)Minimum revenue=0.0 / middle=0.5 / maximum revenue=1.0
PERCENT_RANK() OVER (
  PARTITION BY year             -- Independent rank space for each year
  ORDER BY     revenue_bn       -- Ascending: smallest→0.0, largest→1.0
) * 100                         -- Convert to a percentage (0–100)
Dependent processing with two CTE stages: “calculate PERCENT_RANK” → “retrieve the previous year’s PERCENT_RANK with LAG” has a dependency, so the first CTE establishes pct_rank and the second CTE applies LAG.
Problem

From two years of revenue data for five companies, calculate relative revenue position within each year (pct_rank, percentage rounded to one decimal place) and the year-over-year position change (pct_rank_change). Return company_name, year, revenue_bn, pct_rank, prev_pct_rank, pct_rank_change, sorted by year ascending and then pct_rank descending.

Tables
▸ annual_performance
company_nameyearrevenue_bn
AlphaTech202235.0
BetaSoft202226.0
GammaSys202218.0
DeltaNet202212.0
EpsilonSys20227.0
AlphaTech202342.0
BetaSoft202328.0
GammaSys202315.0
DeltaNet202316.0
EpsilonSys20239.0

※ revenue_bn: revenue (billions of JPY) / from 2022 to 2023: GammaSys decreased to 15.0 and DeltaNet increased to 16.0

Expected Output

Expected output (year ascending → pct_rank descending):

company_nameyearrevenue_bnpct_rankprev_pct_rankpct_rank_change
AlphaTech202235.0100.0NULLNULL
BetaSoft202226.075.0NULLNULL
GammaSys202218.050.0NULLNULL
DeltaNet202212.025.0NULLNULL
EpsilonSys20227.00.0NULLNULL
AlphaTech202342.0100.0100.00.0
BetaSoft202328.075.075.00.0
DeltaNet202316.050.025.025.0
GammaSys202315.025.050.0-25.0
EpsilonSys20239.00.00.00.0

DeltaNet rises the most, +25.0 points, as revenue grows from 12.0 to 16.0 and overtakes GammaSys. GammaSys falls the most, −25.0 points, as revenue drops from 18.0 to 15.0.