Payroll Cost Trend & Variance
Why payroll moved. The month-over-month answer decomposes into people and rates, once the one-time noise is pulled out and named, and it must sum back to the delta exactly or it explains nothing.
◆ Getting the trend honest first— the view, the one-time lines, and the FTE denominator.
| Decision | Why it matters |
|---|---|
| Earned-for view | Labor cost belongs to the period the work happened. This view restates prior months when retro pay lands; that is a feature, and the report shows the restatement rather than hiding it. The paid-in view answers cash questions and gets its own column, never a blend. |
| One-time lines out | Retro catch-ups, off-cycle payments, and severance distort a trend read. They stay in total cost, shown as named lines above the run-rate, so the decomposition works on the recurring base. |
| Overtime separated | An overtime spike is an operations story, not a compensation story. Its own line, read next to the Overtime report. |
| FTE, not heads | The people factor uses full-time equivalents from the snapshot. Two part-time hires are one FTE of cost pressure, and a heads-based bridge misstates it. |
◆ The decomposition, and the SQL— two factors, exact by construction, on the payroll fact and the snapshot.
Cost is FTE times cost per FTE, so the month-over-month change splits into a people effect priced at last month's rate and a rate effect on this month's people. Priced that way, the two terms sum to the delta exactly:
-- month-over-month bridge: recurring fully loaded cost, decomposed
WITH m AS (
SELECT f.earned_for_period_key AS period,
SUM(f.amount) AS cost,
MAX(s.fte) AS fte
FROM fct_payroll_result f
JOIN dim_pay_component pc ON f.pay_component_key = pc.pay_component_key
JOIN (SELECT date_key, SUM(fte) AS fte FROM fct_worker_snapshot GROUP BY 1) s
ON s.date_key = f.earned_for_period_key
WHERE pc.in_fully_loaded
AND NOT pc.is_one_time AND NOT pc.is_overtime
AND f.earned_for_period_key IN (202604, 202605)
GROUP BY 1
)
SELECT curr.cost - prev.cost AS delta,
(curr.fte - prev.fte) * (prev.cost / prev.fte) AS fte_effect,
(curr.cost / curr.fte - prev.cost / prev.fte) * curr.fte AS rate_effect
FROM m curr JOIN m prev ON prev.period = 202604 AND curr.period = 202605
Sample output, April to May, the months the rest of the catalog uses:
| April | May | bridge | |
|---|---|---|---|
| Recurring cost | 2,044,800 | 2,086,340 | +41,540 |
| FTE | 144.0 | 146.0 | FTE effect +28,400 |
| Cost per FTE | 14,200 | 14,290 | Rate effect +13,140 |
28,400 plus 13,140 is 41,540: the bridge closes. The people effect is the two May hires the Movement report nets out; the rate effect is the merit cycle landing in run-rate. A variance the bridge cannot place goes back to the one-time lines or to the reconciliation, in that order. Sample values are illustrative, never client data.
- run-rate
- Recurring fully loaded cost, one-time lines removed. The trend that means something.
- variance bridge
- The delta split into named effects that sum back exactly.
- FTE effect
- The change in people, priced at last month's cost per FTE.
- rate effect
- The change in cost per FTE, on this month's people.
- restatement
- Prior months moving when retro pay lands on the earned-for view. Shown, not hidden.
- one-time lines
- Retro, off-cycle, severance. In total cost, out of run-rate.