工业工程

工业工程

首页
专业认知
课程体系
方法与工具
软件技能
就业发展
考研深造
院校学科
行业洞察
文章
关于
登录 →
工业工程

工业工程

首页 专业认知 课程体系 方法与工具 软件技能 就业发展 考研深造 院校学科 行业洞察 文章 关于
登录
  1. 首页
  2. 软件技能
  3. SQL 工业工程数据分析实战:从 MES 取数到指标看板

SQL 工业工程数据分析实战:从 MES 取数到指标看板

0
  • 软件技能
  • 发布于 2026-01-08
  • 7 次阅读
伴读书童
伴读书童
目录
当前文章没有目录

SQL 工业工程数据分析实战:从 MES 取数到指标看板

引言:IE 最大的时间黑洞是"等数据"

先描述一个几乎所有 IE 都经历过的场景。

周一早上,生产经理问:"上周 A 线的 OEE 是多少?比前周降了不少,是什么原因?"

你开始行动:

  1. 打开 MES,发现系统里只有"设备综合效率"的一个汇总数字,没有明细
  2. 想看停机明细,需要导出 Excel——导出上限 10000 行,一周数据有 30 万行,导不完
  3. 找 IT 提数,IT 说"排期到周三"
  4. 周三拿到数据,发现字段口径不对(IT 的"可用时间"包含了计划停机,你要的是不含)
  5. 再提一次需求,周五拿到
  6. 这时候问题已经过去一周了

这不是能力问题,是工具链问题。 一个会 SQL 的 IE,同样的需求 20 分钟就能拿到准确数据。

更关键的是:会 SQL 意味着你不再依赖别人的口径定义。你可以自己去看原始表、理解字段含义、按自己的逻辑聚合。这在数据口径混乱的制造业尤为宝贵——同一个"良率",质量部用一次通过率,生产部用最终良率,财务用入库/投料比,三个数字差好几个百分点。只有看到底层数据,才知道到底发生了什么。

本文假定读者零基础,从"数据库是什么"讲起,最终能独立写出生产数据分析的查询。


一、为什么 IE 要学 SQL

1.1 数据在哪儿

工厂的数据散落在这些系统里:

系统 英文 核心数据 IE 用它做什么
ERP 企业资源计划 订单、BOM、库存、采购、成本、财务 订单结构分析、成本核算、库存周转
MES 制造执行系统 工单执行、报工、设备状态、工艺参数、质量记录 产能分析、OEE、追溯、瓶颈识别
WMS 仓储管理系统 库位、出入库、库存、拣选任务 仓储效率、拣选路径、库存准确
QMS 质量管理系统 检验记录、不良、CAPA、SPC 不良分析、质量追溯
EAM 设备资产管理系统 设备台账、维修工单、备件、点检 MTBF/MTTR、维修成本
APS 高级计划排程 排程结果、约束、交期 排程可行性、交期分析
SCADA / 数采 数据采集与监控 设备实时参数、报警 工艺参数分析、预测性维护
PLM / PDM 产品生命周期 图纸、BOM 版本、工艺路线 工艺路线分析、变更影响

这些系统的底层几乎都是关系型数据库(SQL Server、Oracle、MySQL、PostgreSQL、达梦、人大金仓等国产库)。只要你能连上(或拿到只读账号、或拿到数据导出),就能用 SQL 查询。

1.2 SQL vs Excel vs Python

工具 适合 不适合
SQL 从数据库取数、聚合、关联多表 复杂统计建模、可视化
Excel 小数据(<10 万行)快速查看、简单透视 大数据、可复用的流程
Python 复杂清洗、建模、自动化、机器学习 简单的取数(杀鸡用牛刀)
Power BI 可视化、看板、交互式分析 数据清洗(Power Query 可以但重)

最优组合:

SQL 从数据库取出"干净、正确粒度"的数据
   ↓
Power BI / Python 做可视化和建模
   ↓
Excel 做临时查看和汇报

关键认知:取数的粒度和口径应该在 SQL 层解决,不要在 Excel 里做。因为 SQL 里的逻辑是可复用的(下次改个日期就能重跑),Excel 里的操作是不可复用的(下个月要重做一遍)。

1.3 学习投入产出比

SQL 的语法核心(SELECT / WHERE / GROUP BY / JOIN / 窗口函数)大约 20 小时能掌握,是所有数据技能中投入产出比最高的:

  • 学会后立刻能解决 80% 的取数需求
  • 语法稳定(SQL 标准几十年没大变),学一次用十年
  • 是数据岗、分析岗、产品经理岗的通用门槛技能

二、关系型数据库基础

2.1 核心概念

概念 说明 类比
数据库(Database) 数据的容器 一个 Excel 文件
表(Table) 数据的二维结构 Excel 的一个 sheet
行(Row / Record) 一条记录 一行
列(Column / Field) 一个字段 一列
主键(Primary Key) 唯一标识一行的列 身份证号
外键(Foreign Key) 指向另一表主键的列 表格间的引用
索引(Index) 加速查询的数据结构 书的目录
视图(View) 保存的查询 一个"虚拟表"
存储过程 保存在数据库里的一段程序 宏
Schema 表的命名空间/分组 文件夹

2.2 为什么是"关系型"

制造业的数据天然是结构化的、强关联的:

客户表 ──< 订单表 ──< 工单表 ──< 报工记录表
                        │
                        ├──< 质量检验表
                        └──< 物料消耗表
产品表 ──< BOM表 ──< 物料表
设备表 ──< 设备状态记录表

"──<" 表示一对多关系:一个客户有多个订单,一个订单有多个工单,一个工单有多条报工记录。

关系型数据库的设计原则就是"拆表 + 关联":把数据拆成多个表避免冗余,查询时用 JOIN 关联起来。

这对 IE 的启示:取数时必须理解表与表之间的关联关系,否则会犯"重复计数"的经典错误(见第六章)。

2.3 常见数据库及 SQL 方言差异

数据库 厂商 制造业常见度 方言差异
SQL Server 微软 极高(中小企业首选) T-SQL,用 TOP n 限制行数;GETDATE() 取当前时间
Oracle 甲骨文 高(大企业) PL/SQL,用 ROWNUM;SYSDATE
MySQL Oracle(开源) 中高(互联网、新系统) LIMIT n;NOW()
PostgreSQL 开源 中(新系统、国产替代基础) LIMIT n;NOW();功能最全的开源库
达梦 DM 国产 中(信创替代) 兼容 Oracle
人大金仓 Kingbase 国产 中(信创替代) 基于 PostgreSQL
OceanBase 阿里 中 兼容 MySQL/Oracle

核心语法(SELECT / WHERE / GROUP BY / JOIN)在所有方言里完全一致,差异主要在:

  • 限制行数:TOP n(SQL Server)vs LIMIT n(MySQL/PG)vs ROWNUM(Oracle)
  • 日期函数:差异最大
  • 字符串拼接:+(SQL Server)vs CONCAT() 或 ||(MySQL/PG/Oracle)
  • 分页:OFFSET-FETCH(SQL Server 2012+)vs LIMIT offset, n(MySQL)

建议:先精通一种(推荐 SQL Server 或 MySQL,资料最多),其他方言用到时查差异即可。


三、SQL 核心语法

3.1 最基础的骨架

SELECT    -- 要哪些列
FROM      -- 从哪张表
WHERE     -- 行过滤条件
GROUP BY  -- 按什么分组
HAVING    -- 分组后的过滤
ORDER BY  -- 排序
LIMIT     -- 限制行数

执行顺序(重要,与书写顺序不同):

1. FROM      确定数据来源
2. WHERE     行级过滤
3. GROUP BY  分组
4. HAVING    组级过滤
5. SELECT    选择列
6. ORDER BY  排序
7. LIMIT     限制

理解执行顺序能解释很多"为什么这样写会报错"的问题。比如 WHERE 里不能用 SELECT 里定义的别名(因为 WHERE 先执行),但 ORDER BY 里可以。

3.2 SELECT 与列操作

-- 基础查询
SELECT work_order_no, product_code, plan_qty, actual_qty
FROM mes_work_order;

-- 全部列(生产环境慎用,字段多时慢)
SELECT * FROM mes_work_order;

-- 去重
SELECT DISTINCT product_code FROM mes_work_order;

-- 计算列与别名
SELECT
    work_order_no,
    plan_qty,
    actual_qty,
    actual_qty - plan_qty AS diff_qty,
    ROUND(actual_qty * 1.0 / plan_qty * 100, 2) AS complete_rate_pct
FROM mes_work_order;

关键陷阱:整数除法。在 SQL Server 里 1/2 = 0(整数除法),必须写 1.0/2 或 CAST(1 AS FLOAT)/2。计算比率时永远先把分子或分母转成浮点。

3.3 WHERE 条件

-- 比较运算
WHERE plan_qty > 1000
WHERE product_code = 'A001'
WHERE actual_start_time >= '2026-01-01'

-- 范围
WHERE plan_qty BETWEEN 100 AND 1000
WHERE product_code IN ('A001', 'A002', 'A003')

-- 模糊匹配
WHERE product_name LIKE '%轴承%'    -- 包含"轴承"
WHERE work_order_no LIKE 'WO2026%'  -- 以 WO2026 开头

-- 空值判断(重要!)
WHERE actual_end_time IS NULL       -- 正确
WHERE actual_end_time = NULL        -- 错误!永远返回空

-- 组合
WHERE status = 'CLOSED'
  AND actual_end_time >= '2026-01-01'
  AND (product_code IN ('A001','A002') OR priority = 'URGENT')

-- 取反
WHERE status NOT IN ('CANCELLED', 'DRAFT')

NULL 的三值逻辑:SQL 里 NULL 表示"未知",任何与 NULL 的比较结果都是 UNKNOWN(既非 TRUE 也非 FALSE),而 WHERE 只保留 TRUE 的行。所以:

  • WHERE col = NULL → 永远返回 0 行
  • WHERE col <> 'A' → 不会返回 col 为 NULL 的行
  • 要包含 NULL:WHERE col IS NULL OR col <> 'A'

这是 SQL 最常见的数据丢失来源。

3.4 聚合函数

函数 作用 对 NULL 的处理
COUNT(*) 行数(含 NULL) 计算所有行
COUNT(col) 行数 跳过 NULL
SUM(col) 求和 跳过 NULL
AVG(col) 平均 跳过 NULL(分母也不计)
MAX(col) / MIN(col) 最大/最小 跳过 NULL
STDEV(col) 样本标准差 跳过 NULL
VAR(col) 样本方差 跳过 NULL
STRING_AGG(col, ',') 字符串聚合(SQL Server 2017+) 跳过 NULL

COUNT(*) vs COUNT(col) 的差异是第二个常见的数据错误来源。

3.5 GROUP BY

-- 按产线统计工单数与产量
SELECT
    line_code,
    COUNT(*)                    AS order_count,
    SUM(plan_qty)               AS total_plan,
    SUM(actual_qty)             AS total_actual,
    ROUND(AVG(actual_qty * 1.0 / plan_qty) * 100, 2) AS avg_complete_pct
FROM mes_work_order
WHERE status = 'CLOSED'
GROUP BY line_code
ORDER BY total_actual DESC;

核心规则:SELECT 里出现的列,要么在 GROUP BY 里,要么在聚合函数里。这条规则在很多数据库(MySQL 的宽松模式除外)会强制检查。

HAVING(对分组结果过滤):

-- 只显示工单数超过 10 的产线
SELECT line_code, COUNT(*) AS order_count
FROM mes_work_order
GROUP BY line_code
HAVING COUNT(*) > 10;

-- HAVING 与 WHERE 的区别
-- WHERE  在分组【前】过滤行(不能用聚合函数)
-- HAVING 在分组【后】过滤组(通常用聚合函数)

3.6 JOIN(最关键的技能)

四种 JOIN:

类型 含义 结果
INNER JOIN 内连接 只保留两边都能匹配上的行
LEFT JOIN 左连接 保留左表所有行,右表匹配不到填 NULL
RIGHT JOIN 右连接 保留右表所有行(少用,通常用 LEFT 换顺序)
FULL OUTER JOIN 全外连接 两边都保留
CROSS JOIN 交叉连接 笛卡尔积(行数的乘积,慎用)
-- 工单 + 产品名称
SELECT
    wo.work_order_no,
    wo.plan_qty,
    p.product_name,
    p.unit
FROM mes_work_order wo
LEFT JOIN md_product p
    ON wo.product_code = p.product_code;

为什么这里用 LEFT JOIN 而不是 INNER JOIN?

如果用 INNER JOIN,那些 product_code 在产品表里找不到对应记录的工单会被静默丢弃。在制造业,主数据不完整(产品编码输错、新产品还没建主数据)是常态。用 INNER JOIN 会导致总数对不上,而你完全不知道丢了多少行。

实务原则:默认用 LEFT JOIN,取完数后检查关键字段的 NULL 比例,确认没有大量匹配失败。如果发现大量 NULL,说明关联键有问题(编码不一致、有空格、大小写问题)。

多表关联:

SELECT
    wo.work_order_no,
    p.product_name,
    l.line_name,
    e.equip_name,
    SUM(rpt.good_qty) AS total_good
FROM mes_work_order wo
LEFT JOIN md_product p   ON wo.product_code = p.product_code
LEFT JOIN md_line l      ON wo.line_code = l.line_code
LEFT JOIN md_equip e     ON wo.equip_code = e.equip_code
LEFT JOIN mes_report rpt ON wo.work_order_no = rpt.work_order_no
GROUP BY wo.work_order_no, p.product_name, l.line_name, e.equip_name;

一对多关联导致的重复计数(最重要陷阱,见第六章详解):

上面的查询里,一个工单有多条报工记录(1:N)。如果直接 SUM(wo.plan_qty),工单的计划数量会被重复计算 N 次。这是 SQL 取数最经典的错误。

3.7 子查询与 CTE

子查询(Subquery):

-- 找出产量高于平均产量的工单
SELECT work_order_no, actual_qty
FROM mes_work_order
WHERE actual_qty > (
    SELECT AVG(actual_qty) FROM mes_work_order
);

CTE(Common Table Expression,公用表表达式)——强烈推荐:

WITH line_summary AS (
    SELECT
        line_code,
        COUNT(*) AS order_count,
        SUM(actual_qty) AS total_qty
    FROM mes_work_order
    WHERE status = 'CLOSED'
    GROUP BY line_code
)
SELECT
    line_code,
    order_count,
    total_qty,
    RANK() OVER (ORDER BY total_qty DESC) AS qty_rank
FROM line_summary
WHERE order_count > 5
ORDER BY qty_rank;

CTE 的价值:把复杂查询拆成"逻辑步骤",每一步单独命名,可读性远超嵌套子查询。用 WITH 开头,可以串联多个:

WITH
base AS (...),      -- 第一步:取基础数据
agg  AS (...),      -- 第二步:聚合
final AS (...)      -- 第三步:计算指标
SELECT * FROM final;

这是写生产数据分析查询的标准做法。任何超过 20 行的查询都应该用 CTE 组织。

3.8 窗口函数(进阶但极其实用)

窗口函数是 SQL 从"能取数"到"能分析"的分水岭。

核心语法:函数名() OVER (PARTITION BY 分组列 ORDER BY 排序列)

函数 作用 IE 场景
ROW_NUMBER() 行号(1,2,3…) 取每个工单的最新一条记录
RANK() 排名(并列跳号:1,1,3) 产量排名
DENSE_RANK() 排名(并列不跳号:1,1,2)
LAG(col, n) 前 n 行的值 算相邻工序的时间间隔
LEAD(col, n) 后 n 行的值 算下一道工序的开始时间
SUM() OVER 累计求和 累积产出曲线
AVG() OVER 移动平均 趋势平滑
FIRST_VALUE() 窗口内第一个值 对比基准

经典应用 1:取每个工单的最新状态

WITH ranked AS (
    SELECT
        work_order_no,
        status,
        update_time,
        ROW_NUMBER() OVER (
            PARTITION BY work_order_no
            ORDER BY update_time DESC
        ) AS rn
    FROM mes_wo_status_log
)
SELECT work_order_no, status, update_time
FROM ranked
WHERE rn = 1;   -- 只取每个工单最新的一条

经典应用 2:算工单在各工序的停留时间

WITH flow AS (
    SELECT
        work_order_no,
        operation_seq,
        operation_name,
        start_time,
        end_time,
        LEAD(start_time) OVER (
            PARTITION BY work_order_no
            ORDER BY operation_seq
        ) AS next_start_time
    FROM mes_operation_record
)
SELECT
    work_order_no,
    operation_name,
    DATEDIFF(MINUTE, start_time, end_time) AS process_min,
    DATEDIFF(MINUTE, end_time, next_start_time) AS wait_min  -- 工序间等待
FROM flow;

这一条查询直接给出了价值流分析的核心数据:哪个工序加工时间长,哪个工序之间等待久。等待时间占比就是纯粹的浪费。

经典应用 3:7 日移动平均(趋势平滑)

SELECT
    stat_date,
    daily_output,
    AVG(daily_output) OVER (
        ORDER BY stat_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS ma7
FROM daily_output_summary
ORDER BY stat_date;

ROWS BETWEEN 6 PRECEDING AND CURRENT ROW 定义了"当前行 + 前 6 行"共 7 行的窗口。

3.9 日期与时间处理(制造业最常用)

不同数据库日期函数差异最大,这里以 SQL Server 为主,标注其他库的等价写法。

-- 当前时间
GETDATE()                        -- SQL Server
NOW()                            -- MySQL / PostgreSQL
SYSDATE                          -- Oracle

-- 日期截断
CAST(GETDATE() AS DATE)          -- 只要日期部分
CONVERT(VARCHAR(7), GETDATE(), 120)  -- '2026-09' 年月

-- 日期加减
DATEADD(DAY, -7, GETDATE())      -- 7 天前
DATEADD(MONTH, -1, GETDATE())    -- 1 个月前

-- 日期差
DATEDIFF(DAY, start_time, end_time)    -- 天数差
DATEDIFF(MINUTE, start_time, end_time) -- 分钟差(最常用)
DATEDIFF(SECOND, start_time, end_time) -- 秒差

-- 提取部分
YEAR(GETDATE()), MONTH(GETDATE()), DAY(GETDATE())
DATEPART(WEEKDAY, GETDATE())     -- 星期几
DATEPART(HOUR, GETDATE())        -- 小时(班次分析用)

-- 构造日期
DATEFROMPARTS(2026, 9, 1)        -- SQL Server 2012+

班次划分(制造业特有需求):

-- 根据小时判断班次(白班 8:00-20:00,夜班 20:00-次日8:00)
CASE
    WHEN DATEPART(HOUR, record_time) >= 8
     AND DATEPART(HOUR, record_time) < 20 THEN '白班'
    ELSE '夜班'
END AS shift

注意跨天班次的日期归属:夜班 20:00 到次日 8:00 属于"哪一天"?通常用"班次日期"(班次开始那天的日期)而不是自然日期,否则夜班数据会被劈成两半。

-- 班次日期 = 如果是 0-8 点,归属前一天
CASE
    WHEN DATEPART(HOUR, record_time) < 8
    THEN DATEADD(DAY, -1, CAST(record_time AS DATE))
    ELSE CAST(record_time AS DATE)
END AS shift_date

3.10 CASE WHEN(条件逻辑)

SELECT
    work_order_no,
    DATEDIFF(HOUR, actual_start, actual_end) AS duration_h,
    CASE
        WHEN DATEDIFF(HOUR, actual_start, actual_end) <= 4  THEN '1. 短(≤4h)'
        WHEN DATEDIFF(HOUR, actual_start, actual_end) <= 12 THEN '2. 中(4-12h)'
        WHEN DATEDIFF(HOUR, actual_start, actual_end) <= 24 THEN '3. 长(12-24h)'
        ELSE '4. 超长(>24h)'
    END AS duration_bucket
FROM mes_work_order;

CASE 在聚合中的用法(条件聚合,极实用):

SELECT
    line_code,
    COUNT(*) AS total_order,
    SUM(CASE WHEN status = 'CLOSED' THEN 1 ELSE 0 END) AS closed_order,
    SUM(CASE WHEN actual_qty >= plan_qty THEN 1 ELSE 0 END) AS on_target_order,
    SUM(CASE WHEN defect_qty > 0 THEN defect_qty ELSE 0 END) AS total_defect
FROM mes_work_order
GROUP BY line_code;

这一招能在一个查询里同时算出多个口径的统计,避免写多个查询再拼。


四、制造业典型表结构

理解 MES/ERP 的数据模型,是写出正确查询的前提。以下是通用的逻辑模型(实际字段各家不同,但结构相似)。

4.1 主数据(Master Data)

表名(示例) 内容 关键字段
md_product 产品主数据 product_code, product_name, spec, unit, product_family
md_bom BOM parent_code, child_code, qty_per, unit, level, effective_date
md_routing 工艺路线 product_code, operation_seq, operation_name, work_center, std_time
md_workcenter 工作中心 wc_code, wc_name, line_code, capacity, cost_rate
md_equipment 设备台账 equip_code, equip_name, model, install_date, status, line_code
md_line 产线 line_code, line_name, workshop, shift_pattern
md_employee 人员 emp_no, emp_name, dept, skill_level, shift
md_supplier 供应商 supplier_code, supplier_name, category, grade
md_customer 客户 customer_code, customer_name, industry
md_warehouse / md_location 仓库/库位 wh_code, loc_code, zone, capacity

4.2 业务数据(Transaction Data)

表名(示例) 内容 粒度 关键字段
erp_sales_order 销售订单 一行 = 一个订单行 so_no, line_no, customer_code, product_code, qty, due_date
mes_work_order 工单 一行 = 一个工单 wo_no, so_no, product_code, plan_qty, plan_start, plan_end, actual_start, actual_end, status, line_code
mes_report 报工记录 一行 = 一次报工 report_id, wo_no, emp_no, equip_code, report_time, good_qty, defect_qty, scrap_qty, operation_seq
mes_equip_status 设备状态 一行 = 一次状态变化 equip_code, status, start_time, end_time, duration, reason_code
mes_downtime 停机记录 一行 = 一次停机 equip_code, start_time, end_time, duration_min, reason_code, reason_desc, category
qms_inspection 检验记录 一行 = 一次检验 insp_id, wo_no, product_code, insp_type, insp_time, sample_qty, defect_qty, result
qms_defect 不良明细 一行 = 一个不良 defect_id, insp_id, defect_code, defect_qty, severity, location
mes_material_consume 物料消耗 一行 = 一次发料/消耗 wo_no, material_code, qty, consume_time, batch_no, supplier_code
wms_inventory_txn 库存事务 一行 = 一次出入库 txn_id, material_code, wh_code, loc_code, txn_type, qty, txn_time, batch_no
eam_maintenance 维修工单 一行 = 一张维修单 mt_no, equip_code, mt_type, start_time, end_time, cost, fault_desc

4.3 ER 关系图(文字版)

md_customer ──< erp_sales_order ──< mes_work_order ──< mes_report
                                          │
md_product ──< md_routing ────────────────┤
    │                                      ├──< qms_inspection ──< qms_defect
    └──< md_bom ──< md_material            ├──< mes_material_consume
                                           └──< mes_downtime

md_line ──< md_workcenter ──< md_equipment ──< mes_equip_status
                                    └──< eam_maintenance

理解粒度的意义:

  • mes_work_order 的粒度是工单——一行一个工单,所以 COUNT(*) 就是工单数
  • mes_report 的粒度是报工——一个工单多次报工,所以关联后 COUNT(*) 是报工次数,不是工单数
  • qms_defect 的粒度是不良项——一次检验可能有多个不良项

取数前必做的一件事:确认每张表的粒度,以及表之间的关系是 1:1、1:N 还是 N:M。

4.4 如何快速摸清一个陌生数据库

拿到数据库权限后,按以下步骤:

-- 1. 看有哪些表
SELECT name FROM sys.tables ORDER BY name;          -- SQL Server
SHOW TABLES;                                         -- MySQL
SELECT table_name FROM information_schema.tables;    -- 通用

-- 2. 看表结构
EXEC sp_columns 'mes_work_order';                    -- SQL Server
DESC mes_work_order;                                 -- MySQL
SELECT * FROM information_schema.columns
WHERE table_name = 'mes_work_order';                 -- 通用

-- 3. 看数据量
SELECT COUNT(*) FROM mes_work_order;

-- 4. 抽样看数据
SELECT TOP 100 * FROM mes_work_order;                -- SQL Server
SELECT * FROM mes_work_order LIMIT 100;              -- MySQL/PG

-- 5. 看关键字段的取值分布(理解枚举值含义)
SELECT status, COUNT(*) AS cnt
FROM mes_work_order
GROUP BY status
ORDER BY cnt DESC;

-- 6. 看时间范围
SELECT MIN(create_time), MAX(create_time) FROM mes_work_order;

第 5 步极其重要。status 字段可能有 '01','02','99' 这样的编码,或者 'DRAFT','RELEASED','CLOSED' 这样的英文,或者有你没想到的 'CANCELLED'。必须先摸清取值分布,才知道 WHERE 该怎么写。


五、十二个 IE 高频查询模板

以下模板以 SQL Server 语法为主,表名用通用命名,实际使用时替换为真实表名。

模板 1:OEE 计算

OEE = 时间开动率 × 性能开动率 × 合格品率

WITH equip_time AS (
    SELECT
        equip_code,
        CAST(shift_date AS DATE) AS stat_date,
        SUM(CASE WHEN status = 'RUNNING' THEN duration_min ELSE 0 END) AS running_min,
        SUM(CASE WHEN status = 'IDLE'    THEN duration_min ELSE 0 END) AS idle_min,
        SUM(CASE WHEN status = 'FAULT'   THEN duration_min ELSE 0 END) AS fault_min,
        SUM(CASE WHEN status = 'SETUP'   THEN duration_min ELSE 0 END) AS setup_min,
        SUM(duration_min) AS total_min
    FROM mes_equip_status
    WHERE shift_date >= '2026-08-01' AND shift_date < '2026-09-01'
    GROUP BY equip_code, CAST(shift_date AS DATE)
),
prod AS (
    SELECT
        equip_code,
        CAST(report_time AS DATE) AS stat_date,
        SUM(good_qty)   AS good_qty,
        SUM(defect_qty) AS defect_qty,
        COUNT(DISTINCT wo_no) AS wo_count
    FROM mes_report
    WHERE report_time >= '2026-08-01' AND report_time < '2026-09-01'
    GROUP BY equip_code, CAST(report_time AS DATE)
)
SELECT
    t.equip_code,
    t.stat_date,
    -- 时间开动率 = 运行时间 / 负荷时间(总时间 - 计划停机)
    ROUND(t.running_min * 1.0 / NULLIF(t.total_min - t.idle_min, 0) * 100, 1) AS availability_pct,
    -- 性能开动率 = 理论加工时间 / 实际运行时间
    ROUND(p.good_qty * 1.0 / NULLIF(t.running_min, 0) * 100, 1) AS performance_pct,
    -- 合格品率
    ROUND(p.good_qty * 1.0 / NULLIF(p.good_qty + p.defect_qty, 0) * 100, 2) AS quality_pct
FROM equip_time t
LEFT JOIN prod p
    ON t.equip_code = p.equip_code AND t.stat_date = p.stat_date
ORDER BY t.equip_code, t.stat_date;

要点:

  • NULLIF(x, 0) 防止除零(返回 NULL 而不是报错)
  • 性能开动率需要"理论节拍"参数,这里简化为产量/运行时间
  • OEE 的三个因子千万不要分别算完再平均,正确做法是:先算每班次的三率,再相乘得每班次的 OEE,最后对 OEE 求平均。或者更严谨:用总的时间/产量直接算

模板 2:工单周期时间分析

WITH wo_cycle AS (
    SELECT
        wo_no,
        product_code,
        line_code,
        plan_qty,
        actual_start,
        actual_end,
        DATEDIFF(MINUTE, actual_start, actual_end) AS cycle_min,
        DATEDIFF(MINUTE, plan_start, actual_end) AS lead_min,
        CASE WHEN actual_end > plan_end THEN 1 ELSE 0 END AS is_delay
    FROM mes_work_order
    WHERE status = 'CLOSED'
      AND actual_end >= '2026-01-01'
)
SELECT
    product_code,
    COUNT(*) AS wo_count,
    AVG(cycle_min) AS avg_cycle_min,
    MAX(cycle_min) AS max_cycle_min,
    -- 中位数(SQL Server 用 PERCENTILE_CONT)
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY cycle_min)
        OVER (PARTITION BY product_code) AS median_cycle_min,
    ROUND(AVG(cycle_min * 1.0 / NULLIF(plan_qty, 0)), 2) AS min_per_unit,
    SUM(is_delay) AS delay_count,
    ROUND(SUM(is_delay) * 100.0 / COUNT(*), 1) AS delay_rate_pct
FROM wo_cycle
GROUP BY product_code, cycle_min
ORDER BY wo_count DESC;

注意:中位数和均值差异大,说明分布右偏(少数工单拖了很久)——这正是改善的重点。

模板 3:瓶颈工位识别

WITH op_stat AS (
    SELECT
        operation_seq,
        operation_name,
        wc_code,
        COUNT(DISTINCT wo_no) AS wo_count,
        AVG(DATEDIFF(SECOND, start_time, end_time)) AS avg_process_sec,
        MAX(DATEDIFF(SECOND, start_time, end_time)) AS max_process_sec,
        STDEV(DATEDIFF(SECOND, start_time, end_time)) AS std_process_sec
    FROM mes_operation_record
    WHERE start_time >= DATEADD(DAY, -30, GETDATE())
    GROUP BY operation_seq, operation_name, wc_code
),
op_wait AS (
    SELECT
        operation_seq,
        AVG(wait_before_sec) AS avg_wait_sec
    FROM (
        SELECT
            operation_seq,
            DATEDIFF(SECOND,
                LAG(end_time) OVER (PARTITION BY wo_no ORDER BY operation_seq),
                start_time) AS wait_before_sec
        FROM mes_operation_record
    ) t
    WHERE wait_before_sec IS NOT NULL AND wait_before_sec >= 0
    GROUP BY operation_seq
)
SELECT
    o.operation_seq,
    o.operation_name,
    o.wc_code,
    ROUND(o.avg_process_sec, 1) AS avg_process_sec,
    ROUND(w.avg_wait_sec, 1) AS avg_wait_sec,
    ROUND(o.avg_process_sec * 1.0 /
          NULLIF(o.avg_process_sec + ISNULL(w.avg_wait_sec,0), 0) * 100, 1) AS value_add_pct,
    ROUND(o.std_process_sec, 1) AS std_process_sec
FROM op_stat o
LEFT JOIN op_wait w ON o.operation_seq = w.operation_seq
ORDER BY o.avg_process_sec DESC;

解读:

  • avg_process_sec 大 → 可能瓶颈
  • avg_wait_sec 大 → 该工序在等料/等设备(下游或供应问题)
  • std_process_sec 大 → 波动大,即使均值不高也会造成堵塞
  • value_add_pct 低 → 增值时间占比低,等待浪费大

模板 4:不良帕累托分析

WITH defect_agg AS (
    SELECT
        d.defect_code,
        ISNULL(dc.defect_name, d.defect_code) AS defect_name,
        SUM(d.defect_qty) AS total_qty
    FROM qms_defect d
    LEFT JOIN md_defect_code dc ON d.defect_code = dc.defect_code
    WHERE d.create_time >= DATEADD(MONTH, -3, GETDATE())
    GROUP BY d.defect_code, dc.defect_name
),
with_pareto AS (
    SELECT
        defect_code,
        defect_name,
        total_qty,
        SUM(total_qty) OVER (ORDER BY total_qty DESC
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum_qty,
        SUM(total_qty) OVER () AS grand_total
    FROM defect_agg
)
SELECT
    defect_code,
    defect_name,
    total_qty,
    ROUND(total_qty * 100.0 / grand_total, 2) AS pct,
    ROUND(cum_qty * 100.0 / grand_total, 2) AS cum_pct,
    CASE WHEN cum_qty * 1.0 / grand_total <= 0.8 THEN '★ 关键少数' ELSE '' END AS vital_few
FROM with_pareto
ORDER BY total_qty DESC;

这一条查询直接输出帕累托图所需的全部数据(含累计百分比和"关键少数"标记),可以直接接到 Power BI 画柏拉图。

模板 5:设备停机归因分析

SELECT
    ISNULL(reason_category, '未分类') AS reason_category,
    COUNT(*) AS downtime_count,
    SUM(duration_min) AS total_downtime_min,
    ROUND(AVG(duration_min), 1) AS avg_downtime_min,
    MAX(duration_min) AS max_downtime_min,
    ROUND(SUM(duration_min) * 100.0 /
          SUM(SUM(duration_min)) OVER (), 1) AS pct_of_total,
    -- MTBF = 总运行时间 / 故障次数
    -- MTTR = 总故障时间 / 故障次数
    ROUND(AVG(duration_min), 1) AS mttr_min
FROM mes_downtime
WHERE start_time >= DATEADD(MONTH, -6, GETDATE())
GROUP BY reason_category
ORDER BY total_downtime_min DESC;

要点:按"总停机时长"和"停机次数"两个维度排序,结论可能不同。次数多但每次短(如卡料)与次数少但每次长(如备件等待),改善方向完全不同。

模板 6:在制品(WIP)追踪

WITH wo_status AS (
    SELECT
        wo_no,
        product_code,
        line_code,
        plan_qty,
        SUM(good_qty) AS completed_qty,
        MAX(report_time) AS last_report_time
    FROM mes_report
    GROUP BY wo_no, product_code, line_code, plan_qty
)
SELECT
    w.line_code,
    COUNT(*) AS wip_orders,
    SUM(w.plan_qty - ISNULL(s.completed_qty, 0)) AS wip_qty,
    AVG(DATEDIFF(HOUR, s.last_report_time, GETDATE())) AS avg_stale_hours,
    SUM(CASE WHEN DATEDIFF(DAY, s.last_report_time, GETDATE()) > 3
             THEN 1 ELSE 0 END) AS stale_over_3d
FROM mes_work_order w
LEFT JOIN wo_status s ON w.wo_no = s.wo_no
WHERE w.status = 'RELEASED'   -- 已下达未关闭
GROUP BY w.line_code
ORDER BY wip_qty DESC;

**"呆滞工单"(stale orders)**是现场最常见的隐性问题:工单一直挂着没关闭,占用产能统计、占用物料、掩盖真实进度。

模板 7:产能利用率

WITH daily_capacity AS (
    SELECT
        line_code,
        CAST(shift_date AS DATE) AS stat_date,
        SUM(planned_work_min) AS planned_min,   -- 计划工作分钟(含加班)
        SUM(actual_work_min)  AS actual_min     -- 实际出勤分钟
    FROM mes_attendance
    WHERE shift_date >= DATEADD(MONTH, -1, GETDATE())
    GROUP BY line_code, CAST(shift_date AS DATE)
),
daily_output AS (
    SELECT
        line_code,
        CAST(report_time AS DATE) AS stat_date,
        SUM(good_qty) AS output_qty
    FROM mes_report
    WHERE report_time >= DATEADD(MONTH, -1, GETDATE())
    GROUP BY line_code, CAST(report_time AS DATE)
)
SELECT
    c.line_code,
    SUM(o.output_qty) AS total_output,
    -- 需要标准工时,从工艺路线表取
    ROUND(SUM(o.output_qty * r.std_time_sec) / 60.0, 0) AS earned_min,  -- 挣值工时
    SUM(c.actual_min) AS actual_work_min,
    ROUND(SUM(o.output_qty * r.std_time_sec) / 60.0 * 100.0
          / NULLIF(SUM(c.actual_min), 0), 1) AS labor_efficiency_pct
FROM daily_capacity c
LEFT JOIN daily_output o
    ON c.line_code = o.line_code AND c.stat_date = o.stat_date
LEFT JOIN (
    SELECT product_code, AVG(std_time_sec) AS std_time_sec
    FROM md_routing GROUP BY product_code
) r ON r.product_code = c.product_code
GROUP BY c.line_code;

"挣值工时"(Earned Hours)是 IE 的核心概念:产出量 × 标准工时 = 应该花多少工时。与实际出勤工时对比,就是劳动效率(Labor Efficiency)。这是比"利用率"更准确的效率指标,因为它剔除了"人在但没有产出"的情况。

模板 8:正反向追溯

-- 正向追溯:给定批次号,找出用这个批次料的所有成品工单
WITH RECURSIVE_CTE AS (
    SELECT
        wo_no AS root_wo,
        material_code,
        batch_no,
        wo_no
    FROM mes_material_consume
    WHERE batch_no = 'BATCH20260815'
)
SELECT DISTINCT
    c.root_wo,
    w.product_code,
    w.actual_end,
    s.customer_code
FROM RECURSIVE_CTE c
LEFT JOIN mes_work_order w ON c.wo_no = w.wo_no
LEFT JOIN erp_sales_order s ON w.so_no = s.so_no;
-- 反向追溯:给定成品序列号,找出用了哪些批次的料
SELECT
    w.wo_no,
    w.product_code,
    m.material_code,
    m.batch_no,
    sup.supplier_name,
    m.consume_time
FROM mes_work_order w
LEFT JOIN mes_material_consume m ON w.wo_no = m.wo_no
LEFT JOIN md_supplier sup ON m.supplier_code = sup.supplier_code
WHERE w.serial_no = 'SN20260815000123';

追溯是召回场景的刚需。汽车、医疗、食品行业对此有强制要求(IATF 16949 要求 4 小时内完成正向+反向追溯)。

模板 9:趋势对比(同比/环比)

WITH monthly AS (
    SELECT
        YEAR(report_time) AS yr,
        MONTH(report_time) AS mo,
        line_code,
        SUM(good_qty) AS output_qty,
        SUM(defect_qty) AS defect_qty
    FROM mes_report
    WHERE report_time >= DATEADD(MONTH, -13, GETDATE())
    GROUP BY YEAR(report_time), MONTH(report_time), line_code
),
with_lag AS (
    SELECT
        yr, mo, line_code, output_qty, defect_qty,
        LAG(output_qty) OVER (PARTITION BY line_code ORDER BY yr, mo) AS prev_month_qty,
        LAG(output_qty, 12) OVER (PARTITION BY line_code ORDER BY yr, mo) AS last_year_qty
    FROM monthly
)
SELECT
    yr, mo, line_code, output_qty,
    ROUND((output_qty - prev_month_qty) * 100.0 / NULLIF(prev_month_qty, 0), 1) AS mom_pct,
    ROUND((output_qty - last_year_qty) * 100.0 / NULLIF(last_year_qty, 0), 1) AS yoy_pct,
    ROUND(defect_qty * 100.0 / NULLIF(output_qty + defect_qty, 0), 2) AS defect_rate_pct
FROM with_lag
WHERE yr = YEAR(GETDATE())
ORDER BY line_code, mo;

模板 10:换型时间分析

WITH seq AS (
    SELECT
        equip_code,
        product_code,
        start_time,
        end_time,
        LAG(product_code) OVER (PARTITION BY equip_code ORDER BY start_time) AS prev_product
    FROM mes_operation_record
    WHERE start_time >= DATEADD(MONTH, -3, GETDATE())
),
changeover AS (
    SELECT
        equip_code,
        prev_product,
        product_code,
        DATEDIFF(MINUTE,
            LAG(end_time) OVER (PARTITION BY equip_code ORDER BY start_time),
            start_time) AS changeover_min
    FROM seq
    WHERE prev_product IS NOT NULL
      AND prev_product <> product_code
)
SELECT
    equip_code,
    COUNT(*) AS changeover_count,
    ROUND(AVG(changeover_min * 1.0), 1) AS avg_changeover_min,
    MAX(changeover_min) AS max_changeover_min,
    SUM(changeover_min) AS total_changeover_min,
    -- 换型矩阵:哪两个产品之间换型最耗时
    TOP_1 = NULL
FROM changeover
GROUP BY equip_code
ORDER BY total_changeover_min DESC;

换型矩阵(哪两个产品之间换型最耗时)可进一步按 prev_product + product_code 分组得出,用于优化生产排程顺序(把换型时间长的产品连着做)。

模板 11:人员效率与技能矩阵

SELECT
    e.emp_no,
    e.emp_name,
    e.skill_level,
    COUNT(DISTINCT r.wo_no) AS wo_count,
    SUM(r.good_qty) AS total_output,
    SUM(DATEDIFF(MINUTE, r.start_time, r.end_time)) AS work_min,
    ROUND(SUM(r.good_qty * rt.std_time_sec) / 60.0 * 100.0
          / NULLIF(SUM(DATEDIFF(MINUTE, r.start_time, r.end_time)), 0), 1) AS efficiency_pct,
    -- 多技能:能操作几个工作中心
    COUNT(DISTINCT r.wc_code) AS wc_variety
FROM mes_report r
LEFT JOIN md_employee e ON r.emp_no = e.emp_no
LEFT JOIN md_routing rt ON r.product_code = rt.product_code AND r.operation_seq = rt.operation_seq
WHERE r.report_time >= DATEADD(MONTH, -3, GETDATE())
GROUP BY e.emp_no, e.emp_name, e.skill_level
ORDER BY efficiency_pct DESC;

wc_variety(能操作的工作中心数量)是多能工培养的量化指标,对柔性生产至关重要。

模板 12:库存周转与呆滞

SELECT
    m.material_code,
    m.material_name,
    SUM(i.on_hand_qty) AS on_hand_qty,
    SUM(i.on_hand_qty * i.std_price) AS on_hand_value,
    MAX(t.last_txn_date) AS last_move_date,
    DATEDIFF(DAY, MAX(t.last_txn_date), GETDATE()) AS idle_days,
    CASE
        WHEN DATEDIFF(DAY, MAX(t.last_txn_date), GETDATE()) > 180 THEN '呆滞(>180天)'
        WHEN DATEDIFF(DAY, MAX(t.last_txn_date), GETDATE()) > 90  THEN '呆滞风险(>90天)'
        ELSE '正常'
    END AS idle_flag
FROM wms_inventory i
LEFT JOIN md_material m ON i.material_code = m.material_code
LEFT JOIN (
    SELECT material_code, MAX(txn_time) AS last_txn_date
    FROM wms_inventory_txn
    GROUP BY material_code
) t ON i.material_code = t.material_code
GROUP BY m.material_code, m.material_name
HAVING SUM(i.on_hand_qty * i.std_price) > 10000
ORDER BY on_hand_value DESC;

六、六大陷阱与避坑指南

陷阱 1:一对多关联导致的重复计数

这是 SQL 取数最经典的错误。

-- 错误示例
SELECT
    wo.line_code,
    SUM(wo.plan_qty) AS total_plan_qty   -- 会被重复计算!
FROM mes_work_order wo
LEFT JOIN mes_report rpt ON wo.wo_no = rpt.wo_no
GROUP BY wo.line_code;

如果一个工单有 5 条报工记录,关联后这个工单会出现 5 行,SUM(plan_qty) 就变成了 5 倍。

三种解法:

-- 解法 A:先聚合后关联(推荐)
WITH rpt_agg AS (
    SELECT wo_no, SUM(good_qty) AS good_qty
    FROM mes_report
    GROUP BY wo_no          -- 先按工单聚合,变成 1:1
)
SELECT wo.line_code, SUM(wo.plan_qty), SUM(r.good_qty)
FROM mes_work_order wo
LEFT JOIN rpt_agg r ON wo.wo_no = r.wo_no
GROUP BY wo.line_code;

-- 解法 B:用 DISTINCT
SELECT wo.line_code, SUM(DISTINCT wo.plan_qty)  -- 不推荐,逻辑不清晰

-- 解法 C:子查询内联聚合
SELECT
    wo.line_code,
    SUM(wo.plan_qty) AS total_plan,
    SUM((SELECT SUM(good_qty) FROM mes_report r WHERE r.wo_no = wo.wo_no)) AS total_good
FROM mes_work_order wo
GROUP BY wo.line_code;

自检方法:关联后先 SELECT COUNT(*),看行数是否等于左表的行数。如果显著变多,就有一对多问题。

陷阱 2:NULL 导致的静默数据丢失

  • WHERE col <> 'A' 会丢掉 NULL 的行
  • AVG()、SUM() 跳过 NULL,如果整列都是 NULL 结果是 NULL 不是 0
  • LEFT JOIN 匹配不上会产生 NULL,SUM(NULL) = NULL,导致整个计算变 NULL

防御写法:

SELECT
    line_code,
    ISNULL(SUM(good_qty), 0) AS total_good,          -- SQL Server
    COALESCE(SUM(good_qty), 0) AS total_good2,       -- 通用(推荐)
    ROUND(COALESCE(SUM(good_qty),0) * 100.0 /
          NULLIF(COALESCE(SUM(good_qty),0) + COALESCE(SUM(defect_qty),0), 0), 2) AS yield_pct
FROM mes_report
GROUP BY line_code;

COALESCE(a, b, c) 返回第一个非 NULL 的值,是标准 SQL,各数据库通用。

陷阱 3:日期范围边界

-- 错误:BETWEEN 包含边界,可能重复计数
WHERE report_time BETWEEN '2026-08-01' AND '2026-08-31'
-- 如果有 8月31日 23:59:59.997 的数据,BETWEEN '2026-08-31' 实际是 00:00:00,会漏掉

-- 正确:左闭右开
WHERE report_time >= '2026-08-01'
  AND report_time <  '2026-09-01'

永远用 >= 起始 AND < 下一起始 的写法,这是处理时间范围的标准做法,无论精度(DATE / DATETIME / DATETIME2)都正确。

陷阱 4:时区与班次日期

  • 数据库存的时间是服务器时区吗?UTC 还是本地时间?
  • 跨天夜班的数据归属哪一天?
  • 夏令时(中国没有,但跨国企业有)

做法:确认数据库时区设置,用 GETUTCDATE() vs GETDATE() 明确区分。涉及夜班时,用前面讲过的"班次日期"逻辑。

陷阱 5:GROUP BY 粒度错误

-- 想要"每月产量",但忘了 GROUP BY 月份
SELECT SUM(good_qty) FROM mes_report WHERE ...  -- 只有一行总数

-- 想要"每条线每月产量",但只 GROUP BY 了产线
SELECT line_code, SUM(good_qty)
FROM mes_report GROUP BY line_code  -- 跨月混合了

检查方法:GROUP BY 之后,每一组应该对应业务上的一个"分析单元"。想要几个维度,就 GROUP BY 几个字段。

陷阱 6:性能问题

生产库的表常常有几千万行。一个写得不好的查询能跑几小时,甚至拖垮数据库。

优化要点:

问题 优化
SELECT * 只 SELECT 需要的列
在 WHERE 里对列做函数运算 改成对常量做运算(见下)
大表 JOIN 大表 先各自聚合再 JOIN
没有索引的过滤列 至少在日期、主键、外键上有索引
子查询嵌套太深 改写成 JOIN 或 CTE
一次性查全表 加日期范围限制
-- 慢:对列做函数,索引失效
WHERE YEAR(report_time) = 2026 AND MONTH(report_time) = 8

-- 快:对常量做运算,能用索引
WHERE report_time >= '2026-08-01' AND report_time < '2026-09-01'

职业道德:在生产数据库上跑查询前:

  1. 先确认是只读账号
  2. 大查询用 TOP 100 先试
  3. 避免在生产高峰跑重查询
  4. 有条件的话,在备库 / 数据仓库 / 导出库上跑分析查询

七、从取数到看板:完整链路

7.1 三种交付形态

形态 做法 适用
一次性分析 写查询 → 导出 Excel → 分析 → 出报告 专题分析
可复用查询 把查询存成视图(View)或存储过程 反复使用的指标
自动化看板 SQL 作为 Power BI 的数据源,定时刷新 日常监控

7.2 建视图(View)

CREATE VIEW v_line_daily_oee AS
SELECT ...   -- 前面模板 1 的查询
;

-- 之后可以直接查
SELECT * FROM v_line_daily_oee
WHERE stat_date >= '2026-08-01';

视图的价值:把复杂的口径逻辑固化在数据库层,所有人用同一个视图,口径统一。这是治理"各部门数字对不上"问题的技术手段。

7.3 对接 Power BI

Power BI
  ├─ 数据源:SQL Server / MySQL / PostgreSQL(直连)
  ├─ 查询方式:
  │    ├─ 直接选表(简单,但会拉全表,慢)
  │    └─ 自定义 SQL 查询(推荐,只取需要的粒度)
  ├─ 刷新:网关(Gateway)定时刷新(如每小时)
  └─ 建模:在 Power BI 里建表关系、写 DAX 度量值

关键建议:在 SQL 层做"行级"的聚合和过滤(减少数据量),在 Power BI 层做"度量值"的计算(保持灵活)。不要把十年原始数据全拉进 Power BI 再算。

7.4 数据准确性验证

任何查询写出来后,必须先验证。三种方法:

  1. 总量对账:查询结果的总和,与系统报表/财务数据对比
  2. 抽样核对:随机抽 5 条记录,手工在 MES 界面查,比对字段值
  3. 逻辑检验:
    • 良率在 0~100% 之间
    • 各部分之和 = 总数
    • 各月之和 = 全年

一个真实教训:某次分析中,我算出某线良率 102%。检查后发现是把"返工合格品"在分子分母都各算了一次。任何超过 100% 的比率、任何负数的时间差,都是数据或逻辑有问题的信号。


八、学习路径

8.1 三阶段

阶段 内容 用时 目标
基础 SELECT / WHERE / ORDER BY / 聚合 / GROUP BY 5h 能从单表取数
进阶 JOIN / 子查询 / CTE / CASE / 日期函数 10h 能关联多表、算指标
高阶 窗口函数 / 查询优化 / 执行计划 10h 能做复杂时序分析

8.2 练习环境

方式 说明
安装本地数据库 SQL Server Express(免费)或 MySQL Community(免费)
在线练习 SQLZoo、LeetCode 数据库题库、HackerRank SQL、牛客网 SQL 题库
示例数据库 Microsoft 的 AdventureWorks / Northwind(经典教学库)
真实数据 学校的课程数据、实习单位的导出数据

最有效的练习:用真实业务问题练。比如"我们厂上个月哪条线的 OEE 最低",自己设计表结构、造数据、写查询。抽象的 LeetCode 题目练的是语法,真实场景练的是建模。

8.3 与 Python 的配合

import pandas as pd
import pyodbc

conn = pyodbc.connect(
    'DRIVER={SQL Server};'
    'SERVER=192.168.1.100;'
    'DATABASE=MES;'
    'UID=readonly_user;PWD=xxx'
)

sql = """
SELECT line_code, stat_date, output_qty, defect_qty
FROM v_line_daily_oee
WHERE stat_date >= '2026-01-01'
"""
df = pd.read_sql(sql, conn)
# 后续用 pandas 做分析

什么时候用 Python + SQL 而不是纯 SQL?

  • 需要复杂的数据清洗(正则、字符串处理)
  • 需要统计建模(回归、时间序列预测、聚类)
  • 需要生成图表
  • 需要自动化(定时跑、自动发邮件)

什么时候纯 SQL 就够?

  • 只是取数 + 聚合
  • 查询逻辑固定,存入视图即可
  • 结果直接进 Power BI

九、给 IE 的十条实务建议

  1. 先问"谁用这个数据,做什么决策",再动手写查询。避免取一堆没人看的数据。

  2. 拿到数据库先看表结构和数据分布,不要凭字段名猜测含义。status = '02' 是什么意思,必须问清楚或查数据。

  3. 永远用 LEFT JOIN 起手,然后检查 NULL 比例。INNER JOIN 会静默丢数据。

  4. 时间范围用左闭右开(>= start AND < next_start),避免边界重复或遗漏。

  5. 比率计算先转浮点(* 1.0),用 NULLIF 防除零。

  6. 写完查询先验证:总量对账 + 抽样核对 + 逻辑检验(良率不超过 100%)。

  7. 关联前后检查行数,防止一对多重复计数。

  8. 复杂查询用 CTE 拆解,每个 CTE 代表一个逻辑步骤,写注释。

  9. 把常用指标固化成视图,统一口径,避免每次重写。

  10. 在生产库上要小心:只读账号、先 TOP 100 试跑、避开业务高峰、优先考虑备库。


结语:SQL 是 IE 的"数据主权"

学 SQL 之前,IE 的数据获取是被动的:你提出需求,别人给你数据,你不知道数据怎么来的,也无法验证对错。

学 SQL 之后,你获得了数据主权:可以直接查看原始记录、自己定义口径、随时验证。这带来的不只是效率提升,更是判断力的提升——当你能看到底层数据,你就不再需要争论"良率到底是 96% 还是 98%",因为你可以直接算出两种口径的数字,然后解释它们为什么不同。

最后一点提醒:SQL 是手段,业务理解才是核心。一条查询写得再漂亮,如果指标定义错了(比如把"计划停机"算进了负荷时间),结果就是错的。写好 SQL 的前提,是你真的理解你要衡量的那个业务过程是什么。

相关阅读

  • 工业工程数据与指标看板:从指标定义、采集口径到可视化落地的完整手册:从指标定义卡 12 要素讲到看板落地:OEE 三种分母口径对照(负荷 75.45% / 计划…
  • MES 系统完全指南:从原理、选型到落地实施:MES 是工厂数字化投入最大、失败率也最高的系统之一。本文讲透:MES 到底解决什么问题(E…
  • 工业工程必备软件地图:从 Excel 到 FlexSim,每个阶段该学什么:按学习阶段给出 IE 的软件全景图:Excel、统计分析、仿真建模、CAD、企业系统,并给出…
  • 用 Python 做 IE 数据分析:从工时数据到线平衡计算:四个可直接运行的 IE 场景代码:标准工时自动计算、产线平衡分析、离散事件仿真、运筹优化排产…
  • 工业工程师的 Excel 实战手册:从工时分析、过程能力到线平衡的 30 个技法:面向 IE 场景的 Excel 实操手册:连续测时数据差分还原、IQR 与 3σ 异常值剔除…
相关文章
Visio 与流程图实战:价值流图、工序流程图与业务建模

Visio 与流程图实战:价值流图、工序流程图与业务建模

一张画得对的价值流图,能让改善讨论从「我觉得」变成「数据显示」。本文讲透:工具选型(Visio/draw.io/Mermaid 等十款对比)、流程图四层体系(工序流程/业务泳道/价值流/BPMN)、工序分析五种符号与流程程序图、VSM 完整绘制法(现状图七步、数据框填法、时间线与增值比、未来图设计六问、定拍工序)、泳道图画法、Visio 与 draw.io 高效操作技巧、让图能沟通的九原则,以及一套装配线 VSM 落地案例。

Python 与 VBA:工业工程自动化的两条路径怎么选

Python 与 VBA:工业工程自动化的两条路径怎么选

每周花 4 小时做同一份报表,一年就是 200 小时。本文系统对比两条自动化路径:VBA(Excel 原生、零部署、适合操作 Excel 本身)与 Python(生态强大、适合数据处理与跨系统),给出明确选型决策树;讲透 VBA 核心能力(宏录制、Range、数组优化、事件、代码片段)与 Python 数据处理栈(pandas、openpyxl、数据清洗七步),含五个可复制案例(多文件合并、BOM 比对、工时清洗、批量重命名、自动发邮件报表)和九个新手坑。

MES 系统完全指南:从原理、选型到落地实施

MES 系统完全指南:从原理、选型到落地实施

MES 是工厂数字化投入最大、失败率也最高的系统之一。本文讲透:MES 到底解决什么问题(ERP 和 SCADA 为何解决不了)、ISA-95 五层架构与 MES 定位、十一大核心功能模块、与 ERP/QMS/WMS/SCADA 的边界与集成接口、三种数据采集方式的取舍与老设备改造方案、选型评估六维度、实施五阶段与分批上线策略,以及八个常见陷阱——包括为什么「先上 MES 再理流程」必然失败。

Power BI 工业工程看板实战:从数据到管理驾驶舱

Power BI 工业工程看板实战:从数据到管理驾驶舱

每天早会 25 分钟花在对数字上,根源是没有口径统一、自动刷新的数据源。本文讲透 Power BI 完整链路:何时该用何时不该、Power Query 数据清洗十类操作与逆透视、星型模型与表关系设计、DAX 核心(计算列 vs 度量值、筛选上下文、CALCULATE、迭代函数、时间智能)、十五个生产管理度量值代码、图表选择与避坑、网关刷新与行级安全,以及看板设计七原则和一套 OEE 管理驾驶舱落地案例。

SQL 工业工程数据分析实战:从 MES 取数到指标看板

SQL 工业工程数据分析实战:从 MES 取数到指标看板

IE 日常有 60% 的时间花在等数据上。本文从真实取数场景出发,讲透 SQL 核心语法(SELECT/WHERE/GROUP BY/JOIN/子查询/CTE/窗口函数/日期处理)、MES 与 ERP 的典型表结构与 ER 关系、如何快速摸清陌生数据库、十二个高频实战查询模板(OEE、工单周期、瓶颈识别、不良帕累托、停机归因、WIP 追踪、挣值工时、正反向追溯、同比环比、换型矩阵、技能矩阵、呆滞库存),以及六大陷阱(一对多重复计数、NULL 静默丢数、日期边界、时区班次、粒度错误、生产库性能)。

AutoCAD 工厂布局实战:从平面图到可落地的设施规划

AutoCAD 工厂布局实战:从平面图到可落地的设施规划

一张合格的车间布局图要能直接拿去施工、报消防、做产能核算、给仿真建模。本文系统讲解 IE 用 AutoCAD 做设施规划的完整链路:软件选型、国标制图规范、高频命令速查、设备图块库与动态块建设、SLP 五步法、厂区级与车间级布局要点、消防与安全间距、人机工程尺寸应用,以及从 CAD 到仿真的数字化交付。

目录
当前文章没有目录
Copyright © 2002 CUMT All Rights Reserved. Powered by 中国矿业大学工业工程系.
粤ICP备2024349181号-3