I sent you this chapter. Reply on LinkedIn or by email.
The formulas behind the three tools, and four queries to run before and after a price rise.
| For | Formula | Notes |
|---|---|---|
| Break-even volume loss | r / (m + r) | r: price rise as a share of today’s price. m: contribution margin today. Both as decimals or both as percents. |
| Break-even elasticity | −(r / (m + r)) / r = −1 / (m + r) | If your customers’ response is weaker than this, the rise pays. |
| Contribution change | U(1 − x)(P(1 + r) − C) − U(P − C) | U: units. x: volume lost. P: price. C: variable cost per unit. |
| Grandfather against raise now | N·o·G·c0 + N(1 − b)·o·(H − G)·c1 − N(1 − a)·o·H·c1 | N: customers. o: orders per month each. G: grandfather months. H: horizon. c0, c1: contribution per order at old and new price. a, b: extra loss if raised now and after G. |
| Tier mix | Σ sharei × (pricei − costi) | Average contribution per order. The Good share that erases the gain: (cup − ctoday) / (cup − cgood), where cup is the share-weighted contribution of Better and Best. |
-- last 90 days; sku_costs is a landed-cost table kept by finance
WITH lines AS (
SELECT l.sku,
SUM(l.quantity) AS units,
SUM(l.quantity * l.price - l.discount) AS net_revenue,
SUM(l.quantity * (c.landed_cost + c.fulfillment_cost + c.packaging_cost)) AS unit_costs,
SUM((o.shipping_cost - o.shipping_charged + o.payment_fee)
* (l.quantity * l.price - l.discount)
/ NULLIF(o.subtotal, 0)) AS allocated_order_costs
FROM order_lines l
JOIN orders o ON o.id = l.order_id
JOIN sku_costs c ON c.sku = l.sku
WHERE o.created_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY l.sku
)
SELECT sku, units,
net_revenue / units AS avg_price,
(net_revenue - unit_costs - allocated_order_costs) / units AS contribution_per_unit,
(net_revenue - unit_costs - allocated_order_costs) / net_revenue AS contribution_margin
FROM lines
ORDER BY net_revenue DESC;
Order-level costs are shared across lines by revenue, an approximation; use it consistently. The margin column is m in the break-even formula.
-- weekly units per SKU, 12 weeks before and after the rise
-- :rise_date and the SKU list come from the plan
SELECT l.sku,
DATE_TRUNC('week', o.created_at) AS week,
SUM(l.quantity) AS units,
(o.created_at >= :rise_date) AS after_rise
FROM order_lines l
JOIN orders o ON o.id = l.order_id
WHERE o.created_at BETWEEN :rise_date - INTERVAL '12 weeks'
AND :rise_date + INTERVAL '12 weeks'
AND l.sku IN (:raised_skus)
GROUP BY 1, 2, 4
ORDER BY 1, 2;
Leave out the stock-up weeks, compare four-week averages against each SKU’s break-even, and do the same for SKUs you didn’t raise.
-- customers active in the 90 days before the rise, and the same window a year earlier
WITH base AS (
SELECT customer_id, 'this_year' AS cohort, :rise_date AS start_date
FROM orders
WHERE created_at >= :rise_date - INTERVAL '90 days' AND created_at < :rise_date
GROUP BY customer_id
UNION ALL
SELECT customer_id, 'last_year', :rise_date - INTERVAL '1 year'
FROM orders
WHERE created_at >= :rise_date - INTERVAL '1 year' - INTERVAL '90 days'
AND created_at < :rise_date - INTERVAL '1 year'
GROUP BY customer_id
)
SELECT b.cohort,
COUNT(*) AS customers,
AVG(CASE WHEN EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = b.customer_id
AND o.created_at >= b.start_date
AND o.created_at < b.start_date + INTERVAL '90 days') THEN 1 ELSE 0 END) AS repeat_90,
AVG(CASE WHEN EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = b.customer_id
AND o.created_at >= b.start_date
AND o.created_at < b.start_date + INTERVAL '180 days') THEN 1 ELSE 0 END) AS repeat_180
FROM base b
GROUP BY b.cohort;
Read the first column at day 90 and the second at day 180. If the stock-up window pulled orders forward, count from the day after it closes, for both cohorts.
-- average discount as a share of list, first orders against repeat orders, last 90 days
SELECT CASE WHEN o.customer_order_number = 1 THEN 'first order' ELSE 'repeat order' END AS type,
COUNT(DISTINCT o.id) AS orders,
SUM(l.discount) / NULLIF(SUM(l.quantity * l.price), 0) AS discount_share,
AVG(CASE WHEN o.discount_code IS NOT NULL THEN 1 ELSE 0 END) AS share_with_code
FROM orders o
JOIN order_lines l ON l.order_id = o.id
WHERE o.created_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY 1;
Deeper first-order discounts are expected. Look for repeat customers using public codes meant for strangers, and, grouping by l.sku too, repeat orders at a worse effective price than first orders.
Want to answer the note that sent you here? Reply on LinkedIn or by email.
If this is your store
The free Klaviyo audit reads your own account and scores it. Start with your store’s address. A read-only key gets the full report.
Written by Andrew Lauchner, a growth and retention operator for consumer brands. The paid work is one ninety-day Sprint.
This is one chapter of The Price Rise, which is free and readable in full on a single page with no form in front of it.