数据库设计与 SQL 优化

从数据建模到查询优化 — 给中际旭创数据可视化工程师的 SQL Server 实战手册

01 · 数据建模基础

数据建模是所有数据库设计的第一步。在制造业中,你需要把车间的真实数据(产量、停机、质量)变成数据库里的表结构。

🏭 生活中的类比

数据建模 = 给工厂仓库建货架

想象一个工厂仓库:你需要决定放几个货架、每个货架分几层、每层放什么。数据建模就是做这件事 —— 决定数据库里有多少张表、每张表有哪些列、表和表之间怎么关联。

建模做得好,后面写查询就轻松;建模乱,就像东西乱堆在仓库里,找什么都费劲。

为什么数据建模重要?

建模做得好 vs 做得差的对比
方面建模好建模差
查询速度毫秒级响应几十秒甚至超时
新增指标加一个 JOIN 就行要改十几张表
数据一致维度统一,不会打架同一机台在不同表里名字不同
报表开发SQL 简洁清晰SQL 嵌套五六层

两种核心建模方法

方法说明适用场景
星型模型 (Star Schema)中间一个事实表,周围连维度表,形状像星星数据仓库、报表分析
雪花模型 (Snowflake)维度表再拆子表,形状像雪花复杂维度、节省存储
💡 实际建议

制造业数据可视化项目首选星型模型。原因:JOIN 层级少、查询性能好、前端开发简单。雪花模型虽然更规范,但在仪表盘场景下会增加 JOIN 复杂度。

02 · 事实表与维度表

星型模型的核心就是两种表:事实表维度表。理解它们是学好 SQL 的基础。

📊 Excel 对比理解
数据库概念Excel 里的对应说明
事实表 (Fact)数据工作表放数值数据,比如产量记录表,每行是一条记录
维度表 (Dimension)下拉选项表放描述信息,比如机台名单、产品列表,供"筛选"用
外键 (FK)VLOOKUP 的关联列事实表里存维度表的 ID,就像用编码去 VLOOKUP
JOINVLOOKUP 操作把事实表和维度表连起来,就像 Excel 跨表查找

制造业的事实表

📦 产量记录事实表 (fact_production)
字段名类型说明Excel 类比
idINT (PK)主键行号
machine_idINT (FK)关联机台机台编码
line_idINT (FK)关联产线产线编码
shift_idINT (FK)关联班次班次编码
product_idINT (FK)关联产品产品编码
production_dateDATETIME生产时间日期列
good_countINT良品数数值
total_countINT总数数值
yield_rateDECIMAL良率计算列
⛔ 停机记录事实表 (fact_downtime)
字段名类型说明
idINT (PK)主键
machine_idINT (FK)关联机台
downtime_startDATETIME停机开始时间
downtime_endDATETIME停机结束时间
downtime_typeNVARCHAR(50)停机类型(计划/故障/换线)
reasonNVARCHAR(200)停机原因
duration_minINT停机时长(分钟)
🔍 质量记录事实表 (fact_quality)
字段名类型说明
idINT (PK)主键
production_idINT (FK)关联产量记录
defect_typeNVARCHAR(50)缺陷类型
defect_countINT缺陷数量
inspectorNVARCHAR(50)检验员
inspect_timeDATETIME检验时间

制造业的维度表

📊 维度表 = Excel 里的"下拉选项"数据源

在 Excel 里做数据验证(下拉列表),你需要一张"选项清单"。维度表就是数据库里的"选项清单"——集中管理机台、产线、班次等基础信息。

⚙️ 机台维度表 (dim_machine)
字段名类型示例数据
machine_id (PK)INT1, 2, 3...
machine_nameNVARCHAR(50)M01-光模块组装机
line_idINT所属产线
machine_typeNVARCHAR(30)组装/测试/包装
statusNVARCHAR(10)运行/停机/维修
install_dateDATE2024-01-15
🏭 产线维度表 (dim_line)
字段示例
line_id1, 2, 3
line_nameA线, B线, C线
workshop光模块车间
target_oee0.85
🌙 班次维度表 (dim_shift)
字段示例
shift_id1, 2
shift_name白班, 夜班
start_time08:00, 20:00
end_time20:00, 08:00
📦 产品维度表 (dim_product)
字段名类型示例数据
product_id (PK)INT1, 2, 3...
product_codeNVARCHAR(30)400G-FR4-001
product_nameNVARCHAR(100)400G FR4 光模块
categoryNVARCHAR(30)400G/200G/100G
target_cycle_timeDECIMAL45.0 (秒)

03 · 制造业完整 ER 图

ER 图(Entity-Relationship Diagram)展示了所有表之间的关系。下面是中际旭创光模块制造场景的完整数据模型:

┌─────────────────────────────────────────────────────────────────────────────────┐
│                        制造业数据仓库 — 星型模型 ER 图                          │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                 │
│   ┌──────────────┐        ┌──────────────┐        ┌──────────────┐             │
│   │ dim_machine  │        │  dim_line    │        │  dim_shift   │             │
│   ├──────────────┤        ├──────────────┤        ├──────────────┤             │
│   │ ●machine_id  │        │ ●line_id     │        │ ●shift_id    │             │
│   │  machine_name│        │  line_name   │        │  shift_name  │             │
│   │  line_id (FK)│───┐    │  workshop    │        │  start_time  │             │
│   │  machine_type│   │    │  target_oee  │        │  end_time    │             │
│   │  status      │   │    └──────┬───────┘        └──────┬───────┘             │
│   └──────┬───────┘   │           │                       │                     │
│          │           │           │                       │                     │
│          │           │           │                       │                     │
│          ▼           ▼           ▼                       ▼                     │
│   ┌──────────────────────────────────────────────────────────────┐             │
│   │              fact_production (产量事实表)                     │             │
│   ├──────────────────────────────────────────────────────────────┤             │
│   │  ●id          ○machine_id (FK)    ○line_id (FK)             │             │
│   │  ○shift_id (FK)   ○product_id (FK)   production_date        │             │
│   │  good_count    total_count        yield_rate                 │             │
│   └──────────┬────────────────────────────────────────┬─────────┘             │
│              │                                         │                       │
│              │                                         │                       │
│   ┌──────────▼──────────┐                  ┌──────────▼──────────┐             │
│   │  fact_downtime      │                  │  dim_product        │             │
│   │  (停机事实表)        │                  │  (产品维度表)        │             │
│   ├─────────────────────┤                  ├─────────────────────┤             │
│   │  ●id                │                  │  ●product_id        │             │
│   │  ○machine_id (FK)   │                  │  product_code       │             │
│   │  downtime_start     │                  │  product_name       │             │
│   │  downtime_end       │                  │  category           │             │
│   │  downtime_type      │                  │  target_cycle_time  │             │
│   │  reason             │                  └─────────────────────┘             │
│   │  duration_min       │                                                      │
│   └─────────────────────┘                                                      │
│                                                                                 │
│   ┌─────────────────────┐                                                      │
│   │  fact_quality       │                                                      │
│   │  (质量事实表)        │                                                      │
│   ├─────────────────────┤                                                      │
│   │  ●id                │                                                      │
│   │  ○production_id(FK) │───► fact_production.id                               │
│   │  defect_type        │                                                      │
│   │  defect_count       │                                                      │
│   │  inspector          │                                                      │
│   │  inspect_time       │                                                      │
│   └─────────────────────┘                                                      │
│                                                                                 │
│   图例: ● = 主键 (PK)   ○ = 外键 (FK)   ───► = 关联关系                       │
└─────────────────────────────────────────────────────────────────────────────────┘

创建表的 SQL 语句

SQL Server — 建表语句
-- 1. 产线维度表
CREATE TABLE dim_line (
    line_id        INT PRIMARY KEY,
    line_name      NVARCHAR(50) NOT NULL,
    workshop       NVARCHAR(100),
    target_oee     DECIMAL(5,4) DEFAULT 0.85
);

-- 2. 班次维度表
CREATE TABLE dim_shift (
    shift_id       INT PRIMARY KEY,
    shift_name     NVARCHAR(20) NOT NULL,
    start_time     TIME NOT NULL,
    end_time       TIME NOT NULL
);

-- 3. 产品维度表
CREATE TABLE dim_product (
    product_id     INT PRIMARY KEY IDENTITY(1,1),
    product_code   NVARCHAR(30) UNIQUE NOT NULL,
    product_name   NVARCHAR(100),
    category       NVARCHAR(30),
    target_cycle_time DECIMAL(6,1)
);

-- 4. 机台维度表
CREATE TABLE dim_machine (
    machine_id     INT PRIMARY KEY IDENTITY(1,1),
    machine_name   NVARCHAR(50) NOT NULL,
    line_id        INT REFERENCES dim_line(line_id),
    machine_type   NVARCHAR(30),
    status         NVARCHAR(10) DEFAULT '运行',
    install_date   DATE
);

-- 5. 产量事实表 (核心表)
CREATE TABLE fact_production (
    id             INT PRIMARY KEY IDENTITY(1,1),
    machine_id     INT NOT NULL REFERENCES dim_machine(machine_id),
    line_id        INT NOT NULL REFERENCES dim_line(line_id),
    shift_id       INT NOT NULL REFERENCES dim_shift(shift_id),
    product_id     INT NOT NULL REFERENCES dim_product(product_id),
    production_date DATETIME NOT NULL,
    good_count     INT DEFAULT 0,
    total_count    INT DEFAULT 0,
    yield_rate     AS (CASE WHEN total_count > 0
                     THEN CAST(good_count AS FLOAT) / total_count
                     ELSE 0 END)
);
💡 IDENTITY(1,1) 是什么?

SQL Server 的自增主键,等价于 MySQL 的 AUTO_INCREMENTIDENTITY(1,1) 表示从 1 开始,每次加 1。插入数据时不需要手动指定 id。

04 · SQL Server 特有语法

中际旭创岗位明确要求 SQL Server,它和 MySQL 有不少语法差异。以下是你必须掌握的 SQL Server 特有写法。

4.1 TOP vs LIMIT

MySQL — LIMIT
-- 取前5条产量记录
SELECT *
FROM fact_production
ORDER BY total_count DESC
LIMIT 5;
SQL Server — TOP
-- 取前5条产量记录
SELECT TOP 5 *
FROM fact_production
ORDER BY total_count DESC;
SQL Server — 分页查询 (OFFSET FETCH)
-- 第2页,每页10条 (SQL Server 2012+)
SELECT *
FROM fact_production
ORDER BY production_date DESC
OFFSET 10 ROWS
FETCH NEXT 10 ROWS ONLY;

4.2 日期函数

SQL Server — GETDATE / DATEADD / DATEDIFF
-- 当前时间
SELECT GETDATE()              -- 2026-06-10 14:30:00

-- DATEADD(单位, 数量, 日期)
SELECT DATEADD(DAY, -7, GETDATE())    -- 7天前
SELECT DATEADD(HOUR, -8, GETDATE())   -- 8小时前
SELECT DATEADD(MONTH, -1, GETDATE())  -- 1个月前

-- DATEDIFF(单位, 开始, 结束)
SELECT DATEDIFF(DAY, downtime_start, downtime_end) AS days_down
FROM fact_downtime

-- 实战:查询最近7天的产量
SELECT *
FROM fact_production
WHERE production_date >= DATEADD(DAY, -7, GETDATE());

4.3 ISNULL vs COALESCE

SQL Server — 空值处理
-- ISNULL(): SQL Server 专有,只支持2个参数
SELECT ISNULL(reason, '未记录') AS reason
FROM fact_downtime

-- COALESCE(): ANSI 标准,支持多个参数,返回第一个非NULL值
SELECT COALESCE(reason, downtime_type, '未知') AS reason
FROM fact_downtime
📊 Excel 对比
SQL ServerExcel说明
ISNULL(col, val)IF(ISBLANK(A2), "默认", A2)如果为空就填默认值
COALESCE(a, b, c)IF(ISBLANK(A2), IF(ISBLANK(B2), C2, B2), A2)从多个值里找第一个非空

4.4 STRING_AGG

SQL Server 2017+ — STRING_AGG (字符串聚合)
-- 把每条产线的所有机台名拼成一个字符串
SELECT
    l.line_name,
    STRING_AGG(m.machine_name, ', ') AS machines
FROM dim_line l
JOIN dim_machine m ON l.line_id = m.line_id
GROUP BY l.line_name;
查询结果
line_namemachines
A线M01-光模块组装机, M02-测试仪, M03-包装机
B线M04-光模块组装机, M05-测试仪

4.5 CTE (WITH AS)

CTE(Common Table Expression)就像 SQL 里的"临时变量",把复杂查询拆成可读的步骤。

🎯 Excel 类比

CTE 就像在 Excel 里先做一个辅助列,计算中间结果,然后再用这个辅助列做最终汇总。只不过 CTE 是 SQL 的写法,不需要真的存下来。

SQL Server — CTE 示例:计算每条产线的 OEE
-- 用 CTE 先算每条产线的基础指标
WITH line_kpi AS (
    SELECT
        line_id,
        SUM(good_count) AS total_good,
        SUM(total_count) AS total_output,
        COUNT(*) AS record_count
    FROM fact_production
    WHERE production_date >= DATEADD(DAY, -30, GETDATE())
    GROUP BY line_id
)
-- 然后用 CTE 结果做最终查询
SELECT
    l.line_name,
    k.total_good,
    k.total_output,
    CAST(k.total_good AS FLOAT) / NULLIF(k.total_output, 0)
        AS yield_rate
FROM line_kpi k
JOIN dim_line l ON k.line_id = l.line_id
ORDER BY yield_rate DESC;

4.6 MERGE 语句

SQL Server — MERGE (数据同步/更新插入)
-- 把临时表的数据同步到产量表:有就更新,没有就插入
MERGE INTO fact_production AS target
USING staging_production AS src
    ON target.machine_id = src.machine_id
    AND target.production_date = src.production_date
WHEN MATCHED THEN
    UPDATE SET
        good_count  = src.good_count,
        total_count = src.total_count
WHEN NOT MATCHED THEN
    INSERT (machine_id, line_id, shift_id, product_id,
            production_date, good_count, total_count)
    VALUES (src.machine_id, src.line_id, src.shift_id,
            src.product_id, src.production_date,
            src.good_count, src.total_count);
💡 MERGE 的典型使用场景

ETL 加载数据时非常常用:从 MES 系统导出的数据,需要和数据库里已有的记录做对比。已有 → 更新;没有 → 插入。这就是 MERGE 的用途。

SQL Server vs MySQL 速查表

功能SQL ServerMySQL
取前 N 条SELECT TOP 5SELECT ... LIMIT 5
自增主键IDENTITY(1,1)AUTO_INCREMENT
当前时间GETDATE()NOW()
日期加减DATEADD(DAY, -7, GETDATE())DATE_SUB(NOW(), INTERVAL 7 DAY)
日期差DATEDIFF(DAY, start, end)DATEDIFF(end, start)
字符串拼接STRING_AGG(col, ',')GROUP_CONCAT(col SEPARATOR ',')
空值替换ISNULL(col, val)IFNULL(col, val)
条件更新插入MERGE INTOINSERT ... ON DUPLICATE KEY
分页OFFSET 10 ROWS FETCH NEXT 10LIMIT 10 OFFSET 10
字符串类型NVARCHAR (Unicode)VARCHAR
执行计划SET SHOWPLAN_TEXT ONEXPLAIN

05 · 窗口函数实战

窗口函数是 SQL 里最强大的分析工具,在制造业仪表盘里几乎无处不在。它的核心思想:不对数据分组聚合,而是给每行数据"附加"一个计算结果

🎯 Excel 类比

窗口函数 = Excel 里的带范围的公式。比如你在 C2 写 =SUM($B$2:$B$10),然后下拉填充 —— 每行都有一个 SUM 结果,但数据并没有被"压缩"成一行。窗口函数就是 SQL 版的这个操作。

5.1 OEE 排名 — ROW_NUMBER / RANK

SQL Server — 每条产线的 OEE 排名
SELECT
    l.line_name,
    p.production_date,
    CAST(p.good_count AS FLOAT) / NULLIF(p.total_count, 0) AS oee,
    RANK() OVER (
        PARTITION BY p.production_date
        ORDER BY CAST(p.good_count AS FLOAT)
                   / NULLIF(p.total_count, 0) DESC
    ) AS oee_rank
FROM fact_production p
JOIN dim_line l ON p.line_id = l.line_id
WHERE p.production_date >= DATEADD(DAY, -7, GETDATE());
查询结果
line_nameproduction_dateoeeoee_rank
A线2026-06-090.921
B线2026-06-090.882
C线2026-06-090.853
B线2026-06-080.911
A线2026-06-080.872
ROW_NUMBER vs RANK vs DENSE_RANK
函数相同值时示例 (92,88,88,85)
ROW_NUMBER()每行不同编号1, 2, 3, 4
RANK()并列同名,跳号1, 2, 2, 4
DENSE_RANK()并列同名,不跳号1, 2, 2, 3

5.2 连续停机时间计算 — LAG / LEAD

SQL Server — 计算每次停机间隔了多久
-- LAG: 看前一行   LEAD: 看后一行
SELECT
    m.machine_name,
    d.downtime_start,
    d.downtime_end,
    d.duration_min,
    -- 上一次停机结束时间
    LAG(d.downtime_end) OVER (
        PARTITION BY d.machine_id
        ORDER BY d.downtime_start
    ) AS prev_end,
    -- 计算两次停机间的运行时间(分钟)
    DATEDIFF(MINUTE,
        LAG(d.downtime_end) OVER (
            PARTITION BY d.machine_id
            ORDER BY d.downtime_start
        ),
        d.downtime_start
    ) AS run_time_between_failures
FROM fact_downtime d
JOIN dim_machine m ON d.machine_id = m.machine_id
ORDER BY m.machine_name, d.downtime_start;
查询结果
machine_namedowntime_startprev_endrun_time_between
M01-组装机06-09 10:0006-09 08:3090
M01-组装机06-09 14:0006-09 10:45195
M01-组装机06-10 09:0006-09 15:301050

5.3 累计产量 — SUM OVER

SQL Server — 按天累计产量 (Running Total)
SELECT
    CONVERT(DATE, production_date) AS prod_date,
    l.line_name,
    SUM(total_count) AS daily_output,
    -- 累计产量:从月初到当前行的总和
    SUM(SUM(total_count)) OVER (
        PARTITION BY l.line_name,
                       YEAR(production_date),
                       MONTH(production_date)
        ORDER BY CONVERT(DATE, production_date)
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_output
FROM fact_production p
JOIN dim_line l ON p.line_id = l.line_id
WHERE production_date >= DATEADD(MONTH, -1, GETDATE())
GROUP BY CONVERT(DATE, production_date), l.line_name
ORDER BY l.line_name, prod_date;
查询结果 — 可以看到 A 线的日产量和月累计
prod_dateline_namedaily_outputcumulative_output
2026-06-01A线1,2001,200
2026-06-02A线1,3502,550
2026-06-03A线1,1003,650
2026-06-04A线1,2804,930
2026-06-05A线1,4006,330

5.4 环比增长 — LAG

SQL Server — 日产量环比增长
WITH daily AS (
    SELECT
        CONVERT(DATE, production_date) AS prod_date,
        line_id,
        SUM(total_count) AS daily_output
    FROM fact_production
    GROUP BY CONVERT(DATE, production_date), line_id
)
SELECT
    d.prod_date,
    l.line_name,
    d.daily_output,
    -- 昨天的产量
    LAG(d.daily_output) OVER (
        PARTITION BY d.line_id
        ORDER BY d.prod_date
    ) AS prev_output,
    -- 环比增长率
    CAST(d.daily_output - LAG(d.daily_output) OVER (
        PARTITION BY d.line_id ORDER BY d.prod_date
    ) AS FLOAT)
    / NULLIF(LAG(d.daily_output) OVER (
        PARTITION BY d.line_id ORDER BY d.prod_date
    ), 0) AS mom_rate
FROM daily d
JOIN dim_line l ON d.line_id = l.line_id
ORDER BY l.line_name, d.prod_date;
查询结果
prod_dateline_namedaily_outputprev_outputmom_rate
2026-06-08A线1,3501,200+12.5%
2026-06-09A线1,1001,350-18.5%
2026-06-10A线1,2801,100+16.4%
⚠️ 窗口函数注意事项

1. 窗口函数只能出现在 SELECT 和 ORDER BY 中,不能在 WHERE 里直接过滤窗口函数的结果。

2. 需要过滤窗口函数结果时,用 CTE 包一层再 WHERE。

3. PARTITION BY 类似 GROUP BY,但不会压缩行数。

06 · 时间范围查询

岗位要求"时间范围过滤",这是制造业仪表盘最高频的操作。你需要掌握各种时间维度的查询。

6.1 今日 / 本周 / 本月 / 本季度

SQL Server — 常用时间范围查询模板
-- ============ 今日产量 ============
SELECT l.line_name, SUM(p.total_count) AS today_output
FROM fact_production p
JOIN dim_line l ON p.line_id = l.line_id
WHERE CONVERT(DATE, p.production_date) = CONVERT(DATE, GETDATE())
GROUP BY l.line_name;

-- ============ 本周产量 ============
SELECT l.line_name, SUM(p.total_count) AS week_output
FROM fact_production p
JOIN dim_line l ON p.line_id = l.line_id
WHERE p.production_date >= DATEADD(DAY,
    -- DATEFIRST 决定一周的第一天 (1=周一)
    1 - DATEPART(WEEKDAY, GETDATE()),
    CONVERT(DATE, GETDATE()))
GROUP BY l.line_name;

-- ============ 本月产量 ============
SELECT l.line_name, SUM(p.total_count) AS month_output
FROM fact_production p
JOIN dim_line l ON p.line_id = l.line_id
WHERE YEAR(p.production_date)  = YEAR(GETDATE())
  AND MONTH(p.production_date) = MONTH(GETDATE())
GROUP BY l.line_name;

-- ============ 本季度产量 ============
SELECT l.line_name, SUM(p.total_count) AS quarter_output
FROM fact_production p
JOIN dim_line l ON p.line_id = l.line_id
WHERE YEAR(p.production_date)   = YEAR(GETDATE())
  AND DATEPART(QUARTER, p.production_date)
     = DATEPART(QUARTER, GETDATE())
GROUP BY l.line_name;

6.2 同比 / 环比

SQL Server — 同比(去年同月) 和 环比(上月)
-- 同比:今年6月 vs 去年6月
WITH monthly AS (
    SELECT
        YEAR(production_date)  AS yr,
        MONTH(production_date) AS mo,
        line_id,
        SUM(total_count) AS total_output
    FROM fact_production
    GROUP BY YEAR(production_date),
             MONTH(production_date), line_id
)
SELECT
    cur.yr, cur.mo, l.line_name,
    cur.total_output  AS current_month,
    prev.total_output AS same_month_last_year,
    -- 同比增长率
    CAST(cur.total_output - prev.total_output AS FLOAT)
        / NULLIF(prev.total_output, 0) AS yoy_rate
FROM monthly cur
JOIN monthly prev
    ON cur.line_id = prev.line_id
    AND cur.mo = prev.mo
    AND cur.yr = prev.yr + 1  -- 去年同月
JOIN dim_line l ON cur.line_id = l.line_id
ORDER BY l.line_name, cur.yr, cur.mo;

6.3 滚动平均 (7天移动平均)

SQL Server — 7天移动平均产量
WITH daily AS (
    SELECT
        CONVERT(DATE, production_date) AS prod_date,
        line_id,
        SUM(total_count) AS daily_output
    FROM fact_production
    GROUP BY CONVERT(DATE, production_date), line_id
)
SELECT
    d.prod_date,
    l.line_name,
    d.daily_output,
    -- 7天移动平均 = 当天+前6天的平均值
    AVG(d.daily_output) OVER (
        PARTITION BY d.line_id
        ORDER BY d.prod_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS ma_7day
FROM daily d
JOIN dim_line l ON d.line_id = l.line_id
ORDER BY l.line_name, d.prod_date;
查询结果 — 移动平均平滑了日波动
prod_dateline_namedaily_outputma_7day
06-04A线1,2801,232
06-05A线1,4001,266
06-06A线1,1501,254
06-07A线1,3001,260
06-08A线1,3501,271
06-09A线1,1001,254
06-10A线1,2801,266
📊 Excel 对比
SQL 概念Excel 公式
7天移动平均 (窗口函数)=AVERAGE(B2:B8) 然后下拉填充
PARTITION BY line_id按产线分别做,不交叉
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW选"当前行和上面6行"的范围

6.4 班次查询 (白班 / 夜班)

SQL Server — 按班次统计产量
-- 方法1: JOIN 班次维度表
SELECT
    s.shift_name,
    l.line_name,
    SUM(p.total_count) AS shift_output,
    AVG(CAST(p.good_count AS FLOAT)
        / NULLIF(p.total_count, 0)) AS avg_yield
FROM fact_production p
JOIN dim_shift s ON p.shift_id = s.shift_id
JOIN dim_line l ON p.line_id = l.line_id
WHERE CONVERT(DATE, p.production_date) = CONVERT(DATE, GETDATE())
GROUP BY s.shift_name, l.line_name;

-- 方法2: 用 CASE 根据小时判断班次(没有班次表时)
SELECT
    CASE
        WHEN DATEPART(HOUR, production_date) BETWEEN 8 AND 19
            THEN '白班'
        ELSE '夜班'
    END AS shift,
    l.line_name,
    SUM(total_count) AS output
FROM fact_production p
JOIN dim_line l ON p.line_id = l.line_id
WHERE CONVERT(DATE, p.production_date) = CONVERT(DATE, GETDATE())
GROUP BY CASE
        WHEN DATEPART(HOUR, production_date) BETWEEN 8 AND 19
            THEN '白班'
        ELSE '夜班'
    END, l.line_name;

07 · 查询性能优化

仪表盘要求秒级响应,慢查询是最大的敌人。掌握以下优化技巧是必备技能。

7.1 查看执行计划

SQL Server — 查看执行计划
-- 方法1: 文本形式的执行计划
SET SHOWPLAN_TEXT ON;
GO
SELECT * FROM fact_production
WHERE production_date >= '2026-06-01';
GO
SET SHOWPLAN_TEXT OFF;

-- 方法2: XML 形式 (更详细)
SET STATISTICS XML ON;
SELECT * FROM fact_production
WHERE production_date >= '2026-06-01';
SET STATISTICS XML OFF;

-- 方法3: SSMS 图形化 (推荐)
-- 在 SQL Server Management Studio 中
-- 点击工具栏的 "Include Actual Execution Plan" 按钮
-- 或按 Ctrl+M

7.2 索引策略

🎯 索引 = 书的目录

没有索引 = 从第1页翻到最后找内容(全表扫描)。有索引 = 先翻目录找到页码,直接翻到那一页。代价是:每加一个索引,插入/更新会变慢(要同时更新目录)。

SQL Server — 创建索引
-- 1. 聚集索引 (Clustered) — 物理排序,每表只能一个
-- 通常用主键或最常用的排序字段
CREATE CLUSTERED INDEX ix_prod_date
ON fact_production(production_date);

-- 2. 非聚集索引 (Nonclustered) — 最常用
CREATE NONCLUSTERED INDEX ix_prod_line_date
ON fact_production(line_id, production_date);

-- 3. 覆盖索引 (Covering Index) — 包含所有需要的列
-- 查询不需要回表 (Bookmark Lookup),速度最快
CREATE NONCLUSTERED INDEX ix_prod_covering
ON fact_production(line_id, production_date)
INCLUDE (good_count, total_count);

-- 4. 唯一索引 — 保证数据不重复
CREATE UNIQUE INDEX ix_product_code
ON dim_product(product_code);
索引类型类比适用场景数量限制
聚集索引 (Clustered)字典正文(按拼音排列)主键、范围查询字段每表1个
非聚集索引 (Nonclustered)字典附录的偏旁部首索引WHERE / JOIN / ORDER BY 的字段无限制
覆盖索引 (Covering)索引里就有答案,不用翻正文查询只需要索引包含的列无限制

7.3 慢查询 vs 优化查询对比

慢查询 — 全表扫描
-- ❌ 在列上用函数,索引失效
SELECT *
FROM fact_production
WHERE YEAR(production_date) = 2026
  AND MONTH(production_date) = 6;

-- ❌ SELECT * 取所有列
-- ❌ 没有 LIMIT/TOP
-- ❌ 函数导致索引失效
-- 预计耗时: 2-5秒 (50万行)
优化查询 — 走索引
-- ✅ 用范围查询代替函数
SELECT line_id, SUM(total_count)
FROM fact_production
WHERE production_date
    >= '2026-06-01'
  AND production_date
    < '2026-07-01'
GROUP BY line_id;

-- ✅ 只取需要的列
-- ✅ 范围查询走索引
-- ✅ 已聚合,返回少量行
-- 预计耗时: 50-200ms

7.4 优化检查清单

SQL 优化 Checklist
#优化项说明
1避免 SELECT *只取需要的列,减少 I/O 和网络传输
2WHERE 列上不要用函数YEAR(date)=2026date >= '2026-01-01'
3合理使用索引WHERE / JOIN / ORDER BY 字段建索引
4覆盖索引INCLUDE 查询需要的列,避免回表
5分页查询OFFSET ... FETCH 代替取全部
6避免子查询,改用 JOIN子查询通常比 JOIN 慢
7用 EXISTS 代替 IN (大数据集)EXISTS 找到就停,IN 会计算全部
8避免 OR,改用 UNION ALLOR 可能导致索引失效
SQL Server — 分页查询最佳实践
-- 仪表盘分页加载 (Keyset Pagination 更高效)
-- 方法1: OFFSET FETCH (简单但大数据量偏慢)
SELECT line_id, production_date, total_count
FROM fact_production
ORDER BY production_date DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY;

-- 方法2: Keyset Pagination (推荐,大数据量时快)
-- 第一页
SELECT TOP 50 line_id, production_date, total_count
FROM fact_production
ORDER BY production_date DESC;

-- 下一页 (假设上一页最后一条的日期是 @last_date)
SELECT TOP 50 line_id, production_date, total_count
FROM fact_production
WHERE production_date < @last_date
ORDER BY production_date DESC;

08 · 视图 (View)

视图是存储的 SQL 查询,可以像表一样查询。在仪表盘开发中非常有用。

🎯 Excel 类比

视图 = Excel 里的"自定义视图"Power Query 的查询。你定义好一个查询逻辑(比如"把产量表和机台表 JOIN 起来,只看最近7天"),保存为一个视图名。以后直接 SELECT * FROM 视图名 就能拿到结果,不用每次写长长的 JOIN。

为什么仪表盘要用视图?

好处说明
简化前端 SQL前端只需 SELECT * FROM v_daily_output,不用写复杂 JOIN
统一数据口径多个仪表盘用同一个视图,指标定义一致
权限控制给前端只开放视图权限,不暴露底层表结构
维护方便业务逻辑变了,只改视图定义,前端代码不用动

创建仪表盘常用视图

SQL Server — 创建视图
-- 视图1: 每日产量汇总 (仪表盘首页用)
CREATE VIEW v_daily_output AS
SELECT
    CONVERT(DATE, p.production_date) AS prod_date,
    l.line_name,
    s.shift_name,
    m.machine_name,
    pr.product_name,
    SUM(p.total_count) AS total_output,
    SUM(p.good_count) AS good_output,
    CAST(SUM(p.good_count) AS FLOAT)
        / NULLIF(SUM(p.total_count), 0) AS yield_rate
FROM fact_production p
JOIN dim_line l     ON p.line_id    = l.line_id
JOIN dim_shift s    ON p.shift_id   = s.shift_id
JOIN dim_machine m  ON p.machine_id = m.machine_id
JOIN dim_product pr ON p.product_id = pr.product_id
GROUP BY CONVERT(DATE, p.production_date),
         l.line_name, s.shift_name,
         m.machine_name, pr.product_name;
GO

-- 视图2: OEE 汇总 (OEE 仪表盘用)
CREATE VIEW v_oee_summary AS
SELECT
    l.line_name,
    CONVERT(DATE, p.production_date) AS prod_date,
    SUM(p.good_count) AS good,
    SUM(p.total_count) AS total,
    CAST(SUM(p.good_count) AS FLOAT)
        / NULLIF(SUM(p.total_count), 0) AS quality_rate,
    -- 简化的 OEE = 良率 x 可用率 x 表现率
    -- 这里只展示良率部分,完整 OEE 需要更多数据
    COUNT(DISTINCT p.machine_id) AS active_machines
FROM fact_production p
JOIN dim_line l ON p.line_id = l.line_id
GROUP BY l.line_name,
         CONVERT(DATE, p.production_date);
GO
前端使用视图 — 非常简洁
-- 前端只需要这样写,不用关心底层 JOIN
SELECT *
FROM v_daily_output
WHERE prod_date >= DATEADD(DAY, -7, GETDATE())
  AND line_name = 'A线'
ORDER BY prod_date DESC;

-- 修改视图 (业务逻辑变了)
ALTER VIEW v_daily_output AS
-- 新的查询逻辑...
-- 前端代码完全不需要改!
⚠️ 视图的限制

1. 视图不存储数据,每次查询都重新执行 SQL(可以建索引视图来优化)。

2. 包含 GROUP BY / DISTINCT / 聚合函数的视图不能直接 UPDATE。

3. 视图嵌套太深会影响可读性和性能(不要在视图上再建视图)。

09 · 面试考点

以下是中际旭创等制造业数据岗位高频出现的数据库面试题。

Q1
事实表和维度表的区别?什么是星型模型?

答题要点:

  • 事实表:存业务数据(数值),行数多、增长快。如产量记录、停机记录。
  • 维度表:存描述信息(文本),行数少、变化慢。如机台、产线、班次。
  • 星型模型:中间一个事实表,通过外键连接多个维度表,形状像星星。
  • 优点:JOIN 层级少(只一层),查询性能好,适合 OLAP 分析。
  • 与雪花模型对比:雪花模型维度表会再拆子表(如产线表再关联车间表),更规范但 JOIN 更多。
星型模型 OLAP 数据仓库
Q2
索引有哪些类型?分别在什么场景下使用?

答题要点:

类型特点场景
聚集索引决定数据物理排序,每表一个主键、范围查询(如日期)
非聚集索引独立存储,不影响物理排序WHERE / JOIN / ORDER BY 字段
唯一索引保证值不重复产品编码、员工工号
覆盖索引INCLUDE 额外列,避免回表查询只需索引中的列
复合索引多列组合,遵循最左前缀多条件查询 (line_id + date)
索引 性能优化
Q3
SQL Server 和 MySQL 有什么区别?

答题要点:

方面SQL ServerMySQL
开发商微软Oracle (开源)
取前N条TOP NLIMIT N
自增IDENTITY(1,1)AUTO_INCREMENT
当前时间GETDATE()NOW()
字符串聚合STRING_AGG()GROUP_CONCAT()
分页OFFSET...FETCHLIMIT...OFFSET
存储过程T-SQL,功能强大支持但功能较少
适用场景企业级 .NET 生态Web 应用,LAMP/LEMP
SQL Server MySQL 数据库对比
Q4
遇到慢查询,你的优化步骤是什么?

答题要点(按步骤回答):

1. 定位慢查询 2. 看执行计划 3. 找瓶颈 4. 优化 5. 验证
  1. 定位:用 SQL Server Profiler / sys.dm_exec_query_stats 找到慢查询
  2. 执行计划:看是全表扫描还是索引扫描,找 Table Scan / Clustered Index Scan
  3. 找瓶颈:WHERE 条件有没有索引?是不是 SELECT *?有没有函数导致索引失效?
  4. 优化手段
    • 加索引 / 优化索引
    • 避免 SELECT *,只取需要的列
    • WHERE 列上不用函数
    • 子查询改 JOIN
    • 大查询拆分 + 分页
  5. 验证:对比优化前后的执行时间和 IO 读取
慢查询 执行计划 优化步骤
Q5
什么时候用存储过程?什么时候用应用层代码?

答题要点:

场景用存储过程用应用层 (Python/Node.js)
复杂的数据转换✅ 减少网络传输❌ 数据来回传
批量数据导入✅ 在数据库内执行快❌ 每条都走网络
业务逻辑判断❌ 难以调试和测试✅ 代码可测试、可版本控制
ETL 定时任务✅ 配合 SQL Agent⚠️ 也可以,但需调度框架
报表查询⚠️ 视图更灵活✅ ORM 或原生 SQL

原则:数据密集型操作用存储过程,业务逻辑放应用层。仪表盘场景一般用视图 + 应用层 SQL。

存储过程 架构设计
Q6
什么是数据库连接池?为什么需要它?

答题要点:

  • 问题:每次查询都新建连接 → TCP 握手 + 权限验证 → 慢(几十到几百毫秒)
  • 连接池:预先创建一组连接放在池里,用时取出,用完归还,不销毁
  • 关键参数
    • 最小连接数 (min pool size) — 启动时创建
    • 最大连接数 (max pool size) — 上限,防止打爆数据库
    • 连接超时 (connection timeout) — 等不到连接怎么办
    • 空闲回收 (idle timeout) — 多久没用的连接关掉
  • Node.js 示例mssql 库自带连接池:new sql.ConnectionPool({max: 10, ...})
  • Python 示例SQLAlchemycreate_engine(pool_size=10)
连接池 后端 性能
Q7
事务隔离级别有哪些?为什么重要?

答题要点:

隔离级别脏读不可重复读幻读性能
Read Uncommitted可能可能可能最快
Read Committed (默认)不会可能可能较快
Repeatable Read不会不会可能中等
Serializable不会不会不会最慢

制造业场景:仪表盘查询通常用 Read Committed(默认)就够了。写入产量数据时,如果并发写入需要一致性,可以用 Repeatable Read。报表查询对实时性要求不高,可以用 NOLOCK 提示提升性能。

事务 隔离级别 并发控制
Q8
什么是分库分表?什么场景下需要?

答题要点:

  • 问题:单表数据量超过千万/亿级 → 查询变慢、写入变慢、备份困难
  • 分表 (水平拆分):把一张大表按规则拆成多张小表
    • 按时间拆:fact_production_202601, fact_production_202602...
    • 按产线拆:fact_production_line_a, fact_production_line_b...
    • 按哈希拆:id % 10 → 10张表
  • 分库:不同库部署在不同服务器,分散压力
  • 制造业场景:产量数据按月分表是最常见的做法。历史数据归档到冷存储。
  • 代价:跨表查询变复杂、分布式事务、运维难度增加。能不分就不分。
分库分表 大数据量 架构

本教程覆盖:数据建模(星型模型)→ 事实表与维度表设计 → SQL Server 特有语法 → 窗口函数实战 → 时间范围查询 → 查询性能优化 → 视图使用 → 面试高频考点

配套教程:01-ECharts / 02-Vue3 / 03-Node.js / 04-Docker / 05-Git / 06-Redis / 07-Python / 08-Linux / 09-ETL / 10-前端基础 / 11-Vite