Files
SBrainCO/db/etl_batch1_fill_dims.sql

283 lines
16 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================
-- ETL Batch 1: 填充维度主数据表
-- 日期: 2026-08-02
-- 说明: 从 bill_fact / salary_detail_records / dish_sales_details
-- 聚合数据填充 dim_member / dim_employee / dim_channel / dim_sku
-- 并创建 member_level / position 标准化映射表
-- 执行: psql -d bill_query -f db/etl_batch1_fill_dims.sql
-- ============================================================
-- ============================================================
-- 1. 填充 dim_member — 会员主数据
-- ============================================================
TRUNCATE TABLE analytics.dim_member;
INSERT INTO analytics.dim_member (
member_id, phone_hash, register_channel, register_store, register_date,
member_level, total_orders, total_revenue, last_order_date, status, tags,
created_at, updated_at
)
WITH member_raw AS (
SELECT
member_id,
-- 首笔消费
MIN(opened_at) AS first_order,
MAX(opened_at) AS last_order,
COUNT(DISTINCT bill_no) AS order_count,
SUM(received_total) AS total_revenue,
-- 最新等级(取最近一笔的member_level
(array_agg(member_level ORDER BY opened_at DESC))[1] AS latest_level,
-- 首笔消费门店
(array_agg(store_code ORDER BY opened_at ASC))[1] AS first_store,
-- 首笔消费渠道
(array_agg(
CASE
WHEN meituan_delivery_received > 0 THEN '美团外卖'
WHEN taobao_delivery_received > 0 THEN '淘宝外卖'
WHEN jd_delivery_received > 0 THEN '京东外卖'
WHEN meituan_received > 0 THEN '美团到店'
WHEN douyin_received > 0 THEN '抖音'
WHEN cash_received > 0 THEN '现金'
WHEN alipay_received > 0 THEN '支付宝'
WHEN wechat_received > 0 THEN '微信'
WHEN unionpay_received > 0 THEN '银联'
WHEN credit_received > 0 THEN '挂账'
ELSE '堂食'
END
ORDER BY opened_at ASC
))[1] AS first_channel
FROM analytics.bill_fact
WHERE member_id IS NOT NULL AND member_id != ''
GROUP BY member_id
),
max_date AS (
SELECT MAX(opened_at)::date AS d FROM analytics.bill_fact WHERE opened_at IS NOT NULL
)
SELECT
mr.member_id,
NULL AS phone_hash, -- 无手机号数据
mr.first_channel AS register_channel,
mr.first_store AS register_store,
(mr.first_order AT TIME ZONE 'Asia/Shanghai')::date AS register_date,
-- 标准化会员等级
CASE
WHEN mr.latest_level IN ('1') THEN '普通'
WHEN mr.latest_level IN ('2','3') THEN '银卡'
WHEN mr.latest_level IN ('4','5') THEN '金卡'
WHEN mr.latest_level IN ('6','7','LV6','LV7') THEN '钻石'
ELSE '普通'
END AS member_level,
mr.order_count::integer AS total_orders,
round(mr.total_revenue::numeric, 2) AS total_revenue,
(mr.last_order AT TIME ZONE 'Asia/Shanghai')::date AS last_order_date,
-- 会员状态(基于最新账单日期)
CASE
WHEN (md.d - (mr.last_order AT TIME ZONE 'Asia/Shanghai')::date) <= 30 THEN '活跃'
WHEN (md.d - (mr.last_order AT TIME ZONE 'Asia/Shanghai')::date) <= 90 THEN '沉睡'
ELSE '流失'
END AS status,
-- 标签
CASE
WHEN mr.order_count = 1 THEN ARRAY['新客']
WHEN mr.order_count >= 20 THEN ARRAY['高频']
WHEN mr.total_revenue >= 500 THEN ARRAY['高价值']
ELSE ARRAY[]::TEXT[]
END AS tags,
now(), now()
FROM member_raw mr
CROSS JOIN max_date md;
CREATE INDEX IF NOT EXISTS idx_dim_member_status ON analytics.dim_member(status);
CREATE INDEX IF NOT EXISTS idx_dim_member_level ON analytics.dim_member(member_level);
CREATE INDEX IF NOT EXISTS idx_dim_member_store ON analytics.dim_member(register_store);
-- ============================================================
-- 2. 填充 dim_employee — 员工主数据
-- ============================================================
TRUNCATE TABLE analytics.dim_employee;
INSERT INTO analytics.dim_employee (
employee_id, employee_name, position, store_code, hire_date, leave_date, status,
created_at, updated_at
)
SELECT DISTINCT ON (employee_code)
employee_code,
NULL AS employee_name, -- 薪资表无姓名字段
-- 标准化岗位
CASE
WHEN position LIKE '店长%' OR position = '储备店长' OR position LIKE '见习经理%' OR position = '储备经理' THEN '店长'
WHEN position LIKE '副店%' THEN '副店长'
WHEN position LIKE '前厅经理%' OR position = '大堂经理' OR position = '服务主管' THEN '前厅经理'
WHEN position = '区经理' OR position LIKE '营运经理%' OR position = '营运助理' OR position LIKE '营运总监%' THEN '区经理'
WHEN position = '厨师长' OR position LIKE '厨师长%' OR position = '行政总厨' OR position LIKE '区厨%' OR position LIKE '大区总厨%' OR position LIKE '拉面区厨%' OR position LIKE '拉面总厨%' OR position = '研发总厨' OR position = '研发经理' OR position LIKE '配送中心%总厨%' OR position = '烤鸭总厨' OR position = '西餐总厨' THEN '厨师长'
WHEN position LIKE '拉面师%' OR position = '拉面' OR position = '拉面主管%' OR position = '面工' OR position LIKE '面工%' OR position = '面点' OR position = '面点师' OR position = '打馕' THEN '拉面师'
WHEN position LIKE '厨师%' OR position = '炒锅' OR position = '砧板' OR position = '打荷' OR position = '上什' OR position = '蒸箱' OR position = '出品' OR position = '副厨' OR position = '明档师傅' OR position = '西餐' OR position = '锅底' THEN '厨师'
WHEN position LIKE '配菜师%' OR position = '配菜师' OR position = '切菜师' OR position LIKE '切菜师%' OR position = '切肉师' OR position = '砧板主管' THEN '配菜师'
WHEN position LIKE '凉菜%' OR position = '凉菜' THEN '凉菜师'
WHEN position LIKE '烧烤%' OR position = '烧烤师' OR position LIKE '烧烤工%' OR position = '烧烤师傅' OR position = '烤鸭师' THEN '烧烤师'
WHEN position LIKE '服务员%' OR position = '传菜员' OR position = '迎宾员' OR position = '吧员' THEN '服务员'
WHEN position = '收银员' OR position = '出纳主管' OR position LIKE '%出纳%' THEN '收银员'
WHEN position = '训练员' OR position LIKE '训练员%' OR position = '训练经理' THEN '训练员'
WHEN position LIKE '厨工%' OR position = '兼职工' OR position = '非全兼职工' OR position = '小时工' OR position = '计时工' THEN '厨工'
WHEN position = '保洁员' OR position = '保洁' THEN '保洁'
WHEN position = '洗碗' THEN '洗碗工'
WHEN position LIKE '%组员%' OR position LIKE '%组长%' THEN '中央厨房工'
WHEN position = '库房组员' OR position = '库房专员' OR position = '库房资深专员' THEN '库管'
WHEN position LIKE '副总%' OR position LIKE '执行总裁%' OR position = '首席营销官%' OR position LIKE '%总监%' OR position LIKE '%经理%' OR position LIKE '%主管%' OR position LIKE '%专员%' OR position LIKE '%助理%' OR position = '主管' THEN '管理岗'
ELSE '其他'
END AS position_std,
-- 门店编码:org_level3 中以人名命名的区域无法直接映射到门店,暂取NULL
NULL AS store_code,
-- hire_date: text转date
CASE
WHEN hire_date ~ '^\d{4}-\d{2}-\d{2}$' THEN hire_date::date
WHEN hire_date ~ '^\d{4}/\d{2}/\d{2}$' THEN to_date(hire_date, 'YYYY/MM/DD')
ELSE NULL
END AS hire_date,
-- leave_date: '0' 表示在职
CASE
WHEN leave_date = '0' OR leave_date IS NULL OR leave_date = '' THEN NULL
WHEN leave_date ~ '^\d{4}-\d{2}-\d{2}$' THEN leave_date::date
WHEN leave_date ~ '^\d{4}/\d{2}/\d{2}$' THEN to_date(leave_date, 'YYYY/MM/DD')
ELSE NULL
END AS leave_date,
-- 状态
CASE
WHEN leave_date = '0' OR leave_date IS NULL OR leave_date = '' THEN '在职'
ELSE '离职'
END AS status,
now(), now()
FROM public.salary_detail_records
ORDER BY employee_code, leave_date DESC NULLS LAST;
CREATE INDEX IF NOT EXISTS idx_dim_employee_status ON analytics.dim_employee(status);
CREATE INDEX IF NOT EXISTS idx_dim_employee_position ON analytics.dim_employee(position);
-- ============================================================
-- 3. 填充 dim_channel — 渠道主数据
-- ============================================================
TRUNCATE TABLE analytics.dim_channel;
INSERT INTO analytics.dim_channel (channel_code, channel_name, channel_group, channel_type, platform, commission_rate, is_delivery, sort_order, status)
VALUES
('dinein', '堂食', '堂食', '堂食', '自有', 0, false, 1, '启用'),
('meituan_dm', '美团到店', '堂食', '美团到店', '美团', 0, false, 2, '启用'),
('meituan_wm', '美团外卖', '外卖', '美团外卖', '美团', 15.0, true, 3, '启用'),
('taobao_wm', '淘宝外卖', '外卖', '淘宝外卖', '淘宝', 12.0, true, 4, '启用'),
('jd_wm', '京东外卖', '外卖', '京东到家', '京东', 10.0, true, 5, '启用'),
('douyin', '抖音', '支付', '抖音', '抖音', 0, false, 6, '启用'),
('alipay', '支付宝', '支付', '支付宝', '自有', 0.6, false, 7, '启用'),
('wechat', '微信', '支付', '微信', '自有', 0.6, false, 8, '启用'),
('cash', '现金', '支付', '现金', '自有', 0, false, 9, '启用'),
('unionpay', '银联', '支付', '银联', '自有', 0.5, false, 10, '启用'),
('credit', '挂账', '支付', '挂账', '自有', 0, false, 11, '启用');
-- ============================================================
-- 4. 填充 dim_sku — SKU主数据(从 dish_sales_details 聚合)
-- ============================================================
TRUNCATE TABLE analytics.dim_sku;
INSERT INTO analytics.dim_sku (
sku_code, standard_name, pos_code, category_l1, category_l2,
status, abc_class, unit, created_at, updated_at
)
SELECT
-- SKU编码:DISH- + 序号(用dense_rank生成)
'DISH-' || lpad(dense_rank() OVER (ORDER BY dish_name)::text, 5, '0'),
dish_name,
NULL AS pos_code,
min(category_level1) AS category_l1,
min(category_level2) AS category_l2,
'在售' AS status,
-- 取最新月度ABC分类
(SELECT abc.abc_class FROM analytics.mv_dish_sku_abc_monthly abc
WHERE abc.dish_name = d.dish_name
ORDER BY abc.month_start DESC LIMIT 1) AS abc_class,
min(unit) AS unit,
now(), now()
FROM public.dish_sales_details d
WHERE d.dish_name IS NOT NULL AND d.dish_name != ''
GROUP BY d.dish_name;
-- ============================================================
-- 5. 创建 member_level_mapping 视图 — 会员等级标准化映射
-- ============================================================
CREATE OR REPLACE VIEW analytics.v_member_level_mapping AS
SELECT
member_level AS raw_level,
CASE
WHEN member_level IN ('1') THEN '普通'
WHEN member_level IN ('2','3') THEN '银卡'
WHEN member_level IN ('4','5') THEN '金卡'
WHEN member_level IN ('6','7','LV6','LV7') THEN '钻石'
ELSE '普通'
END AS standard_level,
CASE
WHEN member_level IN ('1') THEN 1
WHEN member_level IN ('2','3') THEN 2
WHEN member_level IN ('4','5') THEN 3
WHEN member_level IN ('6','7','LV6','LV7') THEN 4
ELSE 1
END AS level_sort
FROM (SELECT DISTINCT member_level FROM analytics.bill_fact WHERE member_level IS NOT NULL AND member_level != '') t;
-- ============================================================
-- 6. 创建 v_position_mapping 视图 — 岗位标准化映射
-- ============================================================
CREATE OR REPLACE VIEW analytics.v_position_mapping AS
SELECT DISTINCT
position AS raw_position,
CASE
WHEN position LIKE '店长%' OR position = '储备店长' OR position LIKE '见习经理%' OR position = '储备经理' THEN '店长'
WHEN position LIKE '副店%' THEN '副店长'
WHEN position LIKE '前厅经理%' OR position = '大堂经理' OR position = '服务主管' THEN '前厅经理'
WHEN position = '区经理' OR position LIKE '营运经理%' OR position = '营运助理' OR position LIKE '营运总监%' THEN '区经理'
WHEN position = '厨师长' OR position LIKE '厨师长%' OR position = '行政总厨' OR position LIKE '区厨%' OR position LIKE '大区总厨%' OR position LIKE '拉面区厨%' OR position LIKE '拉面总厨%' OR position = '研发总厨' OR position = '研发经理' OR position LIKE '配送中心%总厨%' OR position = '烤鸭总厨' OR position = '西餐总厨' THEN '厨师长'
WHEN position LIKE '拉面师%' OR position = '拉面' OR position LIKE '拉面主管%' OR position = '面工' OR position LIKE '面工%' OR position = '面点' OR position = '面点师' OR position = '打馕' THEN '拉面师'
WHEN position LIKE '厨师%' OR position = '炒锅' OR position = '砧板' OR position = '打荷' OR position = '上什' OR position = '蒸箱' OR position = '出品' OR position = '副厨' OR position = '明档师傅' OR position = '西餐' OR position = '锅底' THEN '厨师'
WHEN position LIKE '配菜师%' OR position = '配菜师' OR position = '切菜师' OR position LIKE '切菜师%' OR position = '切肉师' OR position = '砧板主管' THEN '配菜师'
WHEN position LIKE '凉菜%' OR position = '凉菜' THEN '凉菜师'
WHEN position LIKE '烧烤%' OR position = '烧烤师' OR position LIKE '烧烤工%' OR position = '烧烤师傅' OR position = '烤鸭师' THEN '烧烤师'
WHEN position LIKE '服务员%' OR position = '传菜员' OR position = '迎宾员' OR position = '吧员' THEN '服务员'
WHEN position = '收银员' OR position = '出纳主管' OR position LIKE '%出纳%' THEN '收银员'
WHEN position = '训练员' OR position LIKE '训练员%' OR position = '训练经理' THEN '训练员'
WHEN position LIKE '厨工%' OR position = '兼职工' OR position = '非全兼职工' OR position = '小时工' OR position = '计时工' THEN '厨工'
WHEN position = '保洁员' OR position = '保洁' THEN '保洁'
WHEN position = '洗碗' THEN '洗碗工'
WHEN position LIKE '%组员%' OR position LIKE '%组长%' THEN '中央厨房工'
WHEN position = '库房组员' OR position = '库房专员' OR position = '库房资深专员' THEN '库管'
WHEN position LIKE '副总%' OR position LIKE '执行总裁%' OR position = '首席营销官%' OR position LIKE '%总监%' OR position LIKE '%经理%' OR position LIKE '%主管%' OR position LIKE '%专员%' OR position LIKE '%助理%' OR position = '主管' THEN '管理岗'
ELSE '其他'
END AS standard_position
FROM public.salary_detail_records
WHERE position IS NOT NULL AND position != '';
-- ============================================================
-- 7. 补录 dim_store 缺失的 region
-- ============================================================
UPDATE analytics.dim_store SET region = '未知区域' WHERE region IS NULL OR region = '';
-- ============================================================
-- 验证结果
-- ============================================================
SELECT 'dim_member' AS table_name, count(*) AS row_count FROM analytics.dim_member
UNION ALL SELECT 'dim_employee', count(*) FROM analytics.dim_employee
UNION ALL SELECT 'dim_channel', count(*) FROM analytics.dim_channel
UNION ALL SELECT 'dim_sku', count(*) FROM analytics.dim_sku
UNION ALL SELECT 'v_member_level_mapping', count(*) FROM analytics.v_member_level_mapping
UNION ALL SELECT 'v_position_mapping', count(*) FROM analytics.v_position_mapping;
-- dim_member 状态分布
SELECT 'dim_member_status' AS check_name, status, count(*) AS cnt
FROM analytics.dim_member GROUP BY status
UNION ALL
SELECT 'dim_member_level', member_level, count(*)
FROM analytics.dim_member GROUP BY member_level
UNION ALL
SELECT 'dim_employee_status', status, count(*)
FROM analytics.dim_employee GROUP BY status
UNION ALL
SELECT 'dim_employee_position', position, count(*)
FROM analytics.dim_employee GROUP BY position
ORDER BY 1, 3 DESC;