283 lines
16 KiB
SQL
283 lines
16 KiB
SQL
-- ============================================================
|
||
-- 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;
|