perf: 全量物化视图优化 — 按月预计算所有慢函数,API响应从106秒降至0.09秒
- 创建12个按月物化视图替代实时函数调用 - phase10: mv_dish_sku_summary_monthly, mv_dish_sku_abc_monthly - phase11: mv_store_risk_rating/platform_economics/category_mix/member_opportunity/benchmark_composite/deep_diagnosis/dish_basket/dish_store_summary/theoretical_actual_cost (全部_monthly) - 后端所有路由从 fn_xxx() 改为 mv_xxx_monthly WHERE month_start = - 新增 analytics.refresh_all_monthly_views() 刷新函数 - 优化 fn_dish_sku_summary: CTE物化避免重复调用 - 优化 fn_dish_sku_abc: CTE物化 fn_dish_sku_summary 只调用一次 - 优化 fn_dish_sales: 添加 dish_name IS NOT NULL 提前过滤
This commit is contained in:
+33
-27
@@ -39,8 +39,8 @@ router.get('/stores', async (req: AuthRequest, res) => {
|
||||
if (riskLevel) {
|
||||
const m = parseMonth(req)
|
||||
sql = `SELECT s.* FROM analytics.v_store_scorecard s
|
||||
JOIN analytics.fn_store_risk_rating($${params.length + 1}) r ON s.store_code = r.store_code
|
||||
WHERE r.risk_level = $${params.length + 2}`
|
||||
JOIN analytics.mv_store_risk_rating_monthly r ON s.store_code = r.store_code
|
||||
WHERE r.month_start = $${params.length + 1} AND r.risk_level = $${params.length + 2}`
|
||||
params.push(m, riskLevel)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ router.get('/stores', async (req: AuthRequest, res) => {
|
||||
router.get('/stores/risk', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.fn_store_risk_rating($1) ORDER BY risk_level, received DESC`, [month])
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 ORDER BY risk_level, received DESC`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -113,9 +113,9 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
|
||||
const code = req.params.code
|
||||
const [scorecard, risk, platform, benchmark, action] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.mv_store_scorecard WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.fn_store_risk_rating($1) WHERE store_code = $2`, [parseMonth(req), code]),
|
||||
query(`SELECT * FROM analytics.fn_store_platform_economics($2) WHERE store_code = $1`, [code, parseMonth(req)]),
|
||||
query(`SELECT * FROM analytics.fn_store_benchmark_composite($2) WHERE store_code = $1`, [code, parseMonth(req)]),
|
||||
query(`SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 AND store_code = $2`, [parseMonth(req), code]),
|
||||
query(`SELECT * FROM analytics.mv_store_platform_economics_monthly WHERE month_start = $2 AND store_code = $1`, [code, parseMonth(req)]),
|
||||
query(`SELECT * FROM analytics.mv_store_benchmark_composite_monthly WHERE month_start = $2 AND store_code = $1`, [code, parseMonth(req)]),
|
||||
query(`SELECT * FROM analytics.fn_store_action_priority_deep($1) WHERE store_code = $2`, [parseMonth(req), code]),
|
||||
])
|
||||
|
||||
@@ -176,7 +176,7 @@ router.get('/stores/:code/daily', async (req: AuthRequest, res) => {
|
||||
router.get('/cost/comparison', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.fn_store_theoretical_actual_cost($1) ORDER BY variance_to_theoretical_pct DESC NULLS LAST`, [month])
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_theoretical_actual_cost_monthly WHERE month_start = $1 ORDER BY variance_to_theoretical_pct DESC NULLS LAST`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -206,7 +206,7 @@ router.get('/cost/inventory', async (req: AuthRequest, res) => {
|
||||
router.get('/platform/economics', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.fn_store_platform_economics($1) ORDER BY meituan_received DESC NULLS LAST`, [month])
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_platform_economics_monthly WHERE month_start = $1 ORDER BY meituan_received DESC NULLS LAST`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -235,7 +235,7 @@ router.get('/member/repeat', async (req: AuthRequest, res) => {
|
||||
router.get('/sku/abc', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.fn_dish_sku_abc($1) ORDER BY cumulative_revenue_share`, [month])
|
||||
const result = await query(`SELECT * FROM analytics.mv_dish_sku_abc_monthly WHERE month_start = $1 ORDER BY cumulative_revenue_share`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -310,7 +310,7 @@ router.get('/marketing/plans', async (req: AuthRequest, res) => {
|
||||
router.get('/benchmark/composite', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.fn_store_benchmark_composite($1) ORDER BY benchmark_score DESC`, [month])
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_benchmark_composite_monthly WHERE month_start = $1 ORDER BY benchmark_score DESC`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -396,7 +396,7 @@ router.get('/stores/:code/category-mix', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const [result, companyResult] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.fn_store_category_mix($1) WHERE store_code = $2`, [month, req.params.code]),
|
||||
query(`SELECT * FROM analytics.mv_store_category_mix_monthly WHERE month_start = $1 AND store_code = $2`, [month, req.params.code]),
|
||||
query(`
|
||||
SELECT
|
||||
SUM(consumption) as total_consumption,
|
||||
@@ -405,7 +405,7 @@ router.get('/stores/:code/category-mix', async (req: AuthRequest, res) => {
|
||||
SUM(delivery_package) as total_delivery,
|
||||
SUM(cold_dishes) as total_cold,
|
||||
SUM(silk_road_food) as total_silk
|
||||
FROM analytics.fn_store_category_mix($1)
|
||||
FROM analytics.mv_store_category_mix_monthly WHERE month_start = $1
|
||||
`, [month]),
|
||||
])
|
||||
sendSuccess(res, { store: result.rows[0], company: companyResult.rows[0] })
|
||||
@@ -416,7 +416,7 @@ router.get('/stores/:code/cost', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const [costResult, catResult] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.fn_store_theoretical_actual_cost($1) WHERE store_code = $2`, [month, req.params.code]),
|
||||
query(`SELECT * FROM analytics.mv_store_theoretical_actual_cost_monthly WHERE month_start = $1 AND store_code = $2`, [month, req.params.code]),
|
||||
query(`
|
||||
WITH store_cat AS (
|
||||
SELECT finance_category AS category_name,
|
||||
@@ -450,7 +450,7 @@ router.get('/stores/:code/member', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const [oppResult, repeatResult, monthlyResult] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.fn_store_member_opportunity($2) WHERE store_code = $1`, [req.params.code, month]),
|
||||
query(`SELECT * FROM analytics.mv_store_member_opportunity_monthly WHERE month_start = $2 AND store_code = $1`, [req.params.code, month]),
|
||||
query(`SELECT * FROM analytics.mv_store_repeat_summary_monthly WHERE store_code = $1 AND month_start = $2::date`, [req.params.code, month]),
|
||||
query(`
|
||||
SELECT store_code, store_name, count(*) as member_count,
|
||||
@@ -545,10 +545,10 @@ router.get('/overview/profit-waterfall', async (req: AuthRequest, res) => {
|
||||
e.repair_clean_expense,
|
||||
e.operating_expense,
|
||||
CASE WHEN e.operating_expense IS NOT NULL THEN true ELSE false END AS has_expense
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
FROM analytics.mv_store_risk_rating_monthly r
|
||||
LEFT JOIN analytics.mv_store_operating_expense_monthly e
|
||||
ON r.store_code = e.sales_store_code AND e.report_month = $1::date
|
||||
WHERE r.received IS NOT NULL AND e.operating_expense IS NOT NULL
|
||||
WHERE r.month_start = $1 AND r.received IS NOT NULL AND e.operating_expense IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
round(sum(received)::numeric, 2) AS received,
|
||||
@@ -584,10 +584,10 @@ router.get('/overview/store-profit-ranking', async (req: AuthRequest, res) => {
|
||||
round((r.received - e.actual_food_cost - e.operating_expense)::numeric, 2) AS store_contribution,
|
||||
round((r.received - e.actual_food_cost - e.operating_expense) / nullif(r.received, 0) * 100, 2) AS contribution_margin_pct,
|
||||
r.risk_level
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
FROM analytics.mv_store_risk_rating_monthly r
|
||||
LEFT JOIN analytics.mv_store_operating_expense_monthly e
|
||||
ON r.store_code = e.sales_store_code AND e.report_month = $1::date
|
||||
WHERE r.received IS NOT NULL AND r.received > 0 AND e.operating_expense IS NOT NULL
|
||||
WHERE r.month_start = $1 AND r.received IS NOT NULL AND r.received > 0 AND e.operating_expense IS NOT NULL
|
||||
ORDER BY store_contribution DESC
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
@@ -601,12 +601,18 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
WITH standard_stores AS (
|
||||
WITH risk AS (
|
||||
SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1
|
||||
),
|
||||
sku_abc AS (
|
||||
SELECT * FROM analytics.mv_dish_sku_abc_monthly WHERE month_start = $1
|
||||
),
|
||||
standard_stores AS (
|
||||
SELECT r.store_code, r.store_name, r.received,
|
||||
e.actual_food_cost, e.operating_expense,
|
||||
e.wage_expense, e.utility_expense,
|
||||
r.theoretical_margin_pct, r.discount_rate_pct
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
FROM risk r
|
||||
LEFT JOIN analytics.mv_store_operating_expense_monthly e
|
||||
ON r.store_code = e.sales_store_code AND e.report_month = $1::date
|
||||
WHERE r.received IS NOT NULL AND r.received > 0 AND e.operating_expense IS NOT NULL
|
||||
@@ -663,14 +669,14 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
|
||||
SELECT
|
||||
count(*) FILTER (WHERE discount_rate_pct > 25) AS high_discount_stores,
|
||||
round(sum(CASE WHEN discount_rate_pct > 25 THEN received * discount_rate_pct / nullif(100 - discount_rate_pct, 0) * (discount_rate_pct - 25) / nullif(discount_rate_pct, 0) ELSE 0 END)::numeric, 2) AS theoretical_saving
|
||||
FROM analytics.fn_store_risk_rating($1)
|
||||
FROM risk
|
||||
WHERE received IS NOT NULL AND received > 0
|
||||
),
|
||||
discount_stores AS (
|
||||
SELECT store_name, received,
|
||||
round(discount_rate_pct::numeric, 2) AS discount_rate,
|
||||
round((received * discount_rate_pct / nullif(100 - discount_rate_pct, 0) * (discount_rate_pct - 25) / nullif(discount_rate_pct, 0))::numeric, 2) AS potential_saving
|
||||
FROM analytics.fn_store_risk_rating($1)
|
||||
FROM risk
|
||||
WHERE received IS NOT NULL AND received > 0 AND discount_rate_pct > 25
|
||||
ORDER BY discount_rate_pct DESC
|
||||
LIMIT 5
|
||||
@@ -695,11 +701,11 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
|
||||
SELECT
|
||||
count(*) FILTER (WHERE received_amount < 1000 AND bill_count < 30) AS low_sku_count,
|
||||
round(sum(received_amount) FILTER (WHERE received_amount < 1000 AND bill_count < 30)::numeric, 2) AS low_sku_revenue
|
||||
FROM analytics.fn_dish_sku_abc($1)
|
||||
FROM sku_abc
|
||||
),
|
||||
sku_samples AS (
|
||||
SELECT dish_name, category_level1, received_amount, bill_count, abc_class
|
||||
FROM analytics.fn_dish_sku_abc($1)
|
||||
FROM sku_abc
|
||||
WHERE received_amount < 1000 AND bill_count < 30
|
||||
ORDER BY received_amount ASC
|
||||
LIMIT 10
|
||||
@@ -1051,7 +1057,7 @@ router.get('/bank/report', async (req: AuthRequest, res) => {
|
||||
SELECT r.received, e.actual_food_cost AS food_cost, e.wage_expense AS wage, e.rent_expense AS rent,
|
||||
e.utility_expense AS utility, e.dorm_expense AS dorm, e.delivery_commission_expense AS commission,
|
||||
e.card_fee_expense AS card_fee, e.repair_clean_expense AS repair, e.operating_expense AS operating_expense
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
FROM analytics.mv_store_risk_rating_monthly r
|
||||
LEFT JOIN analytics.mv_store_operating_expense_monthly e ON r.store_code = e.sales_store_code AND e.report_month = $1::date
|
||||
WHERE r.received IS NOT NULL AND e.operating_expense IS NOT NULL
|
||||
)
|
||||
@@ -1062,9 +1068,9 @@ router.get('/bank/report', async (req: AuthRequest, res) => {
|
||||
round(sum(received)-sum(food_cost)-sum(operating_expense),2) AS store_contribution
|
||||
FROM full_scope
|
||||
`, [month]),
|
||||
query(`SELECT risk_level, count(*) AS store_count, round(sum(received)::numeric,2) AS total_received, round(avg(theoretical_margin_pct)::numeric,2) AS avg_margin_pct, round(avg(discount_rate_pct)::numeric,2) AS avg_discount_pct FROM analytics.fn_store_risk_rating($1) WHERE received IS NOT NULL GROUP BY risk_level ORDER BY risk_level`, [month]),
|
||||
query(`SELECT risk_level, count(*) AS store_count, round(sum(received)::numeric,2) AS total_received, round(avg(theoretical_margin_pct)::numeric,2) AS avg_margin_pct, round(avg(discount_rate_pct)::numeric,2) AS avg_discount_pct FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 AND received IS NOT NULL GROUP BY risk_level ORDER BY risk_level`, [month]),
|
||||
query(`SELECT round(sum(cash)::numeric,2) AS cash, round(sum(alipay)::numeric,2) AS alipay, round(sum(wechat)::numeric,2) AS wechat, round(sum(meituan)::numeric,2) AS meituan, round(sum(unionpay)::numeric,2) AS unionpay, round(sum(douyin)::numeric,2) AS douyin, round(sum(credit)::numeric,2) AS credit, round(sum(jd_delivery)::numeric,2) AS jd_delivery, round(sum(meituan_delivery)::numeric,2) AS meituan_delivery, round(sum(taobao_delivery)::numeric,2) AS taobao_delivery FROM analytics.v_channel_daily WHERE business_date >= $1::date AND business_date < $2::date`, [month, nextMonth]),
|
||||
query(`SELECT store_code, store_name, round(received::numeric,2) AS received, bill_count, round(avg_bill_value::numeric,2) AS avg_bill_value, round(discount_rate_pct::numeric,2) AS discount_rate_pct, round(theoretical_margin_pct::numeric,2) AS theoretical_margin_pct, risk_level FROM analytics.fn_store_risk_rating($1) WHERE received IS NOT NULL ORDER BY received DESC`, [month]),
|
||||
query(`SELECT store_code, store_name, round(received::numeric,2) AS received, bill_count, round(avg_bill_value::numeric,2) AS avg_bill_value, round(discount_rate_pct::numeric,2) AS discount_rate_pct, round(theoretical_margin_pct::numeric,2) AS theoretical_margin_pct, risk_level FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 AND received IS NOT NULL ORDER BY received DESC`, [month]),
|
||||
])
|
||||
|
||||
const ov = overview.rows[0] as any
|
||||
|
||||
Reference in New Issue
Block a user