CORR(Y, X) is an ordinary aggregate function, so it can be combined with GROUP BY to calculate a correlation coefficient for every group in one query. Unlike RANK and similar functions, it cannot be used as a window function.
-- Calculate correlation for every group with GROUP BY + CORR SELECT channel, ROUND(CORR(revenue, ad_cost)::numeric, 4) AS corr_coef, REGR_COUNT(revenue, ad_cost) AS n -- Number of valid pairs excluding NULLs FROM channel_sales GROUP BY channel; -- To attach CORR at row level, OVER() is unavailable → use CTE GROUP BY + JOIN -- ABS(CORR) allows positive and negative correlation to be compared on one strength scale
GROUP BY channel, CORR is calculated from all rows within each channel. If you also need weekly correlations, adjust the grain to something like GROUP BY channel, week_group. If REGR_COUNT is smaller than expected, many NULLs may be reducing the reliability of the model.Using the channel_sales table, calculate, for each channel, the Pearson correlation coefficient (to 4 decimal places) between advertising cost (ad_cost) and revenue, the number of valid pairs (n), and a correlation-strength label (strength). Use ABS(CORR) >= 0.9 → 'Strong correlation', >= 0.7 → 'Moderate correlation', and 'Weak correlation' otherwise. Return the rows in descending corr_coef order.
| week | channel | ad_cost | revenue |
|---|---|---|---|
| 1 | Online | 100 | 280 |
| 2 | Online | 200 | 650 |
| 3 | Online | 300 | 780 |
| 4 | Online | 400 | 1100 |
| 5 | Online | 500 | 1250 |
| 6 | Online | 600 | 1640 |
| 1 | Offline | 100 | 500 |
| 2 | Offline | 200 | 420 |
| 3 | Offline | 300 | 680 |
| 4 | Offline | 400 | 550 |
| 5 | Offline | 500 | 820 |
| 6 | Offline | 600 | 700 |
Expected output (2 rows, descending corr_coef):
| channel | corr_coef | n | strength |
|---|---|---|---|
| Online | 0.9914 | 6 | Strong correlation |
| Offline | 0.7498 | 6 | Moderate correlation |
Online: x̅=350, y̅=950 / Σ deviation product=446,000 / √(175,000×1,156,400)=449,856 → CORR=0.9914 ⁄ Offline: y̅=611.67 / Σ deviation product=103,500 / √(175,000×108,883)=138,038 → CORR=0.7498
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
Attaching the regression residual (residual = observed value − predicted value) to each row helps identify rows that deviate substantially from the model, which are candidates for outliers or anomalies. A standard SQL pattern is aggregate in a CTE (one row) → expand to every row with CROSS JOIN → calculate predictions and residuals.
-- ① Aggregate the model parameters into one row in a CTE WITH model AS ( SELECT REGR_SLOPE(Y, X) AS slope, REGR_INTERCEPT(Y, X) AS intercept FROM data_table ), -- ② Expand the parameters to every row with CROSS JOIN → calculate predictions and residuals with_pred AS ( SELECT d.*, ROUND((m.slope * d.X + m.intercept)::numeric, 0) AS predicted, ROUND((d.Y - (m.slope * d.X + m.intercept))::numeric, 0) AS residual FROM data_table d CROSS JOIN model m -- 1 row × N rows = N rows ) SELECT *, ABS(residual) AS abs_residual FROM with_pred ORDER BY abs_residual DESC;
ABS(residual) are outlier candidates. Also check REGR_R2 to see whether outliers are lowering the coefficient of determination.Using the store_data table, build a linear regression model from floor area (floor_sqm) to daily sales (daily_sales) in a CTE. Calculate each store's predicted sales (predicted), residual (residual), and absolute residual (abs_residual). Return the rows in descending abs_residual order and identify the store that is an outlier candidate.
| store_id | floor_sqm | daily_sales |
|---|---|---|
| S1 | 100 | 500 |
| S2 | 200 | 950 |
| S3 | 300 | 1400 |
| S4 | 400 | 1850 |
| S5 | 500 | 2300 |
| S6 | 600 | 2200 |
| S7 | 700 | 3200 |
| S8 | 800 | 3700 |
Expected output (8 rows, descending abs_residual):
| store_id | floor_sqm | daily_sales | predicted | residual | abs_residual |
|---|---|---|---|---|---|
| S6 | 600 | 2200 | 2664 | -464 | 464 |
| S8 | 800 | 3700 | 3533 | 167 | 167 |
| S7 | 700 | 3200 | 3099 | 101 | 101 |
| S5 | 500 | 2300 | 2230 | 70 | 70 |
| S4 | 400 | 1850 | 1795 | 55 | 55 |
| S3 | 300 | 1400 | 1361 | 39 | 39 |
| S2 | 200 | 950 | 926 | 24 | 24 |
| S1 | 100 | 500 | 492 | 8 | 8 |
slope=4.3452, intercept=57.14, R²=0.9678 ⁄ S6: observed 2,200 versus predicted 2,664 (residual −464) — an outlier candidate whose sales are far below expectation for 600m²
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
Combining GROUP BY with REGR_SLOPE and REGR_INTERCEPT builds an independent regression model for every category in one query. You can then join a new-data table with JOIN to batch-process predictions for multiple scenarios.
-- Build category-specific models in parallel with GROUP BY WITH models AS ( SELECT category, REGR_SLOPE(sales, ad_cost) AS slope, REGR_INTERCEPT(sales, ad_cost) AS intercept, REGR_R2(sales, ad_cost) AS r2 FROM product_data GROUP BY category ) -- Apply each category model to new data with JOIN for batch prediction SELECT f.category, f.future_ad, ROUND((m.slope * f.future_ad + m.intercept)::numeric, 0) AS predicted_sales FROM forecast_input f JOIN models m USING (category);
Using the product_data table, build a regression model from advertising cost (ad_cost) to sales, for each category. Then apply each category's model to the forecast advertising costs (future_ad) in forecast_input, and output predicted sales (predicted_sales), slope, intercept, and coefficient of determination (r2). Return rows in ascending category and future_ad order.
| category | ad_cost | sales |
|---|---|---|
| Electronics | 100 | 300 |
| Electronics | 200 | 720 |
| Electronics | 300 | 1000 |
| Electronics | 400 | 1420 |
| Electronics | 500 | 1660 |
| Fashion | 100 | 220 |
| Fashion | 200 | 300 |
| Fashion | 300 | 480 |
| Fashion | 400 | 490 |
| Fashion | 500 | 680 |
| category | future_ad |
|---|---|
| Electronics | 400 |
| Electronics | 600 |
| Fashion | 400 |
| Fashion | 600 |
Expected output (4 rows, ascending category and future_ad):
| category | future_ad | predicted_sales | slope | intercept | r2 |
|---|---|---|---|---|---|
| Electronics | 400 | 1362 | 3.4200 | -6.00 | 0.9926 |
| Electronics | 600 | 2046 | 3.4200 | -6.00 | 0.9926 |
| Fashion | 400 | 545 | 1.1100 | 101.00 | 0.9513 |
| Fashion | 600 | 767 | 1.1100 | 101.00 | 0.9513 |
Electronics: slope=3.42 (ad spend +100 → sales +342) / Fashion: slope=1.11 (ad spend +100 → sales +111) — advertising ROI is approximately three times higher for Electronics
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
PERCENTILE_CONT(p) WITHIN GROUP (ORDER BY col) is an ordered-set aggregate function that sorts the data in ascending order and returns the value at position p (0–1) by linear interpolation. Q1, the median, and Q3 are obtained with p=0.25, 0.5, and 0.75.
-- Syntax: PERCENTILE_CONT(percentile) WITHIN GROUP (ORDER BY column) -- Linear interpolation: row position = 1 + p×(N−1), e.g. N=12, p=0.25 → position=3.75 PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY score) AS q1 PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY score) AS median PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY score) AS q3 -- PERCENTILE_DISC: return the nearest actual value without interpolation (discrete version) -- Tukey fences (box-plot outlier rule): -- lower_fence = Q1 − 1.5×IQR / upper_fence = Q3 + 1.5×IQR -- Values outside the fences are treated as outliers
GROUP BY category to calculate medians by category at once, but it cannot be used as an OVER() window function. NTILE is an alternative when you need to attach a percentile band to every row, but remember the difference: NTILE divides by row count, whereas PERCENTILE_CONT interpolates by value.Using the exam_results table, calculate IQR, the lower fence, and the upper fence from Q1, the median, and Q3 in a CTE. Label whether each student's score is within the Tukey fences (lower=Q1−1.5×IQR, upper=Q3+1.5×IQR) with outlier_flag ('Outlier' / 'Normal'). Return the rows in ascending score order.
| student_id | score |
|---|---|
| S01 | 67 |
| S02 | 82 |
| S03 | 55 |
| S04 | 138 |
| S05 | 63 |
| S06 | 80 |
| S07 | 42 |
| S08 | 91 |
| S09 | 71 |
| S10 | 75 |
| S11 | 85 |
| S12 | 58 |
Expected output (12 rows, ascending score):
| student_id | score | lower_fence | upper_fence | outlier_flag |
|---|---|---|---|---|
| S07 | 42 | 30.25 | 114.25 | Normal |
| S03 | 55 | 30.25 | 114.25 | Normal |
| S12 | 58 | 30.25 | 114.25 | Normal |
| S05 | 63 | 30.25 | 114.25 | Normal |
| S01 | 67 | 30.25 | 114.25 | Normal |
| S09 | 71 | 30.25 | 114.25 | Normal |
| S10 | 75 | 30.25 | 114.25 | Normal |
| S06 | 80 | 30.25 | 114.25 | Normal |
| S02 | 82 | 30.25 | 114.25 | Normal |
| S11 | 85 | 30.25 | 114.25 | Normal |
| S08 | 91 | 30.25 | 114.25 | Normal |
| S04 | 138 | 30.25 | 114.25 | Outlier |
sorted: 42,55,58,63,67,71,75,80,82,85,91,138 / Q1=61.75, median=73.00, Q3=82.75, IQR=21.00 / lower=30.25, upper=114.25 / only S04(138) exceeds the upper fence
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
Explicitly specifying a window frame with ROWS BETWEEN n PRECEDING AND CURRENT ROW calculates a moving average over the most recent n+1 rows, including the current row.
-- Explicitly specify the moving-average window frame with ROWS BETWEEN AVG(revenue) OVER ( ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS ma3 -- 3-month moving average (latest 3 rows including current month) -- Difference from the default frame: -- ORDER BY alone: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- → cumulative average from the first row through the current month -- ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- → only the latest 3 physical rows (moving average) ← this is the correct frame -- Month-over-month pattern using NULLIF to prevent division by zero (LAG extension) -- (revenue - prev_revenue)::numeric / NULLIF(prev_revenue, 0) * 100
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW (a cumulative frame that also includes rows with the same value). To specify “the most recent N physical rows” for a moving average, always write ROWS BETWEEN explicitly. The results differ when the ORDER BY column contains duplicate values.Using the sales_monthly table, attach the previous month's sales (prev_revenue) with LAG in a CTE. In the outer query, calculate both a 3-month moving average (ma3: the latest 3 rows, rounded to an integer) and the month-over-month growth rate (mom_pct: 2 decimal places, with NULLIF to prevent division by zero). Return rows in ascending month order.
| month | revenue |
|---|---|
| 2024-01 | 1200000 |
| 2024-02 | 1350000 |
| 2024-03 | 1180000 |
| 2024-04 | 1520000 |
| 2024-05 | 1680000 |
| 2024-06 | 1450000 |
| 2024-07 | 1820000 |
| 2024-08 | 1960000 |
Expected output (8 rows, ascending month):
| month | revenue | ma3 | mom_pct |
|---|---|---|---|
| 2024-01 | 1200000 | 1200000 | NULL |
| 2024-02 | 1350000 | 1275000 | 12.50 |
| 2024-03 | 1180000 | 1243333 | -12.59 |
| 2024-04 | 1520000 | 1350000 | 28.81 |
| 2024-05 | 1680000 | 1460000 | 10.53 |
| 2024-06 | 1450000 | 1550000 | -13.69 |
| 2024-07 | 1820000 | 1650000 | 25.52 |
| 2024-08 | 1960000 | 1743333 | 7.69 |
ma3(Jan)=one row=1,200,000 / ma3(Feb)=(1.2M+1.35M)/2=1,275,000 / ma3(Mar)=(1.2M+1.35M+1.18M)/3=1,243,333 / thereafter, a sliding window of the latest 3 rows
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free