数据库设计与 SQL 优化
从数据建模到查询优化 — 给中际旭创数据可视化工程师的 SQL Server 实战手册
01 · 数据建模基础
数据建模是所有数据库设计的第一步。在制造业中,你需要把车间的真实数据(产量、停机、质量)变成数据库里的表结构。
数据建模 = 给工厂仓库建货架
想象一个工厂仓库:你需要决定放几个货架、每个货架分几层、每层放什么。数据建模就是做这件事 —— 决定数据库里有多少张表、每张表有哪些列、表和表之间怎么关联。
建模做得好,后面写查询就轻松;建模乱,就像东西乱堆在仓库里,找什么都费劲。
为什么数据建模重要?
| 方面 | 建模好 | 建模差 |
|---|---|---|
| 查询速度 | 毫秒级响应 | 几十秒甚至超时 |
| 新增指标 | 加一个 JOIN 就行 | 要改十几张表 |
| 数据一致 | 维度统一,不会打架 | 同一机台在不同表里名字不同 |
| 报表开发 | SQL 简洁清晰 | SQL 嵌套五六层 |
两种核心建模方法
| 方法 | 说明 | 适用场景 |
|---|---|---|
| 星型模型 (Star Schema) | 中间一个事实表,周围连维度表,形状像星星 | 数据仓库、报表分析 |
| 雪花模型 (Snowflake) | 维度表再拆子表,形状像雪花 | 复杂维度、节省存储 |
制造业数据可视化项目首选星型模型。原因:JOIN 层级少、查询性能好、前端开发简单。雪花模型虽然更规范,但在仪表盘场景下会增加 JOIN 复杂度。
02 · 事实表与维度表
星型模型的核心就是两种表:事实表和维度表。理解它们是学好 SQL 的基础。
| 数据库概念 | Excel 里的对应 | 说明 |
|---|---|---|
| 事实表 (Fact) | 数据工作表 | 放数值数据,比如产量记录表,每行是一条记录 |
| 维度表 (Dimension) | 下拉选项表 | 放描述信息,比如机台名单、产品列表,供"筛选"用 |
| 外键 (FK) | VLOOKUP 的关联列 | 事实表里存维度表的 ID,就像用编码去 VLOOKUP |
| JOIN | VLOOKUP 操作 | 把事实表和维度表连起来,就像 Excel 跨表查找 |
制造业的事实表
| 字段名 | 类型 | 说明 | Excel 类比 |
|---|---|---|---|
| id | INT (PK) | 主键 | 行号 |
| machine_id | INT (FK) | 关联机台 | 机台编码 |
| line_id | INT (FK) | 关联产线 | 产线编码 |
| shift_id | INT (FK) | 关联班次 | 班次编码 |
| product_id | INT (FK) | 关联产品 | 产品编码 |
| production_date | DATETIME | 生产时间 | 日期列 |
| good_count | INT | 良品数 | 数值 |
| total_count | INT | 总数 | 数值 |
| yield_rate | DECIMAL | 良率 | 计算列 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT (PK) | 主键 |
| machine_id | INT (FK) | 关联机台 |
| downtime_start | DATETIME | 停机开始时间 |
| downtime_end | DATETIME | 停机结束时间 |
| downtime_type | NVARCHAR(50) | 停机类型(计划/故障/换线) |
| reason | NVARCHAR(200) | 停机原因 |
| duration_min | INT | 停机时长(分钟) |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT (PK) | 主键 |
| production_id | INT (FK) | 关联产量记录 |
| defect_type | NVARCHAR(50) | 缺陷类型 |
| defect_count | INT | 缺陷数量 |
| inspector | NVARCHAR(50) | 检验员 |
| inspect_time | DATETIME | 检验时间 |
制造业的维度表
在 Excel 里做数据验证(下拉列表),你需要一张"选项清单"。维度表就是数据库里的"选项清单"——集中管理机台、产线、班次等基础信息。
| 字段名 | 类型 | 示例数据 |
|---|---|---|
| machine_id (PK) | INT | 1, 2, 3... |
| machine_name | NVARCHAR(50) | M01-光模块组装机 |
| line_id | INT | 所属产线 |
| machine_type | NVARCHAR(30) | 组装/测试/包装 |
| status | NVARCHAR(10) | 运行/停机/维修 |
| install_date | DATE | 2024-01-15 |
| 字段 | 示例 |
|---|---|
| line_id | 1, 2, 3 |
| line_name | A线, B线, C线 |
| workshop | 光模块车间 |
| target_oee | 0.85 |
| 字段 | 示例 |
|---|---|
| shift_id | 1, 2 |
| shift_name | 白班, 夜班 |
| start_time | 08:00, 20:00 |
| end_time | 20:00, 08:00 |
| 字段名 | 类型 | 示例数据 |
|---|---|---|
| product_id (PK) | INT | 1, 2, 3... |
| product_code | NVARCHAR(30) | 400G-FR4-001 |
| product_name | NVARCHAR(100) | 400G FR4 光模块 |
| category | NVARCHAR(30) | 400G/200G/100G |
| target_cycle_time | DECIMAL | 45.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 语句
-- 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)
);
SQL Server 的自增主键,等价于 MySQL 的 AUTO_INCREMENT。IDENTITY(1,1) 表示从 1 开始,每次加 1。插入数据时不需要手动指定 id。
04 · SQL Server 特有语法
中际旭创岗位明确要求 SQL Server,它和 MySQL 有不少语法差异。以下是你必须掌握的 SQL Server 特有写法。
4.1 TOP vs LIMIT
-- 取前5条产量记录
SELECT *
FROM fact_production
ORDER BY total_count DESC
LIMIT 5;
-- 取前5条产量记录
SELECT TOP 5 *
FROM fact_production
ORDER BY total_count DESC;
-- 第2页,每页10条 (SQL Server 2012+)
SELECT *
FROM fact_production
ORDER BY production_date DESC
OFFSET 10 ROWS
FETCH NEXT 10 ROWS ONLY;
4.2 日期函数
-- 当前时间
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
-- 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
| SQL Server | Excel | 说明 |
|---|---|---|
| ISNULL(col, val) | IF(ISBLANK(A2), "默认", A2) | 如果为空就填默认值 |
| COALESCE(a, b, c) | IF(ISBLANK(A2), IF(ISBLANK(B2), C2, B2), A2) | 从多个值里找第一个非空 |
4.4 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_name | machines |
|---|---|
| A线 | M01-光模块组装机, M02-测试仪, M03-包装机 |
| B线 | M04-光模块组装机, M05-测试仪 |
4.5 CTE (WITH AS)
CTE(Common Table Expression)就像 SQL 里的"临时变量",把复杂查询拆成可读的步骤。
CTE 就像在 Excel 里先做一个辅助列,计算中间结果,然后再用这个辅助列做最终汇总。只不过 CTE 是 SQL 的写法,不需要真的存下来。
-- 用 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 语句
-- 把临时表的数据同步到产量表:有就更新,没有就插入
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);
ETL 加载数据时非常常用:从 MES 系统导出的数据,需要和数据库里已有的记录做对比。已有 → 更新;没有 → 插入。这就是 MERGE 的用途。
SQL Server vs MySQL 速查表
| 功能 | SQL Server | MySQL |
|---|---|---|
| 取前 N 条 | SELECT TOP 5 | SELECT ... 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 INTO | INSERT ... ON DUPLICATE KEY |
| 分页 | OFFSET 10 ROWS FETCH NEXT 10 | LIMIT 10 OFFSET 10 |
| 字符串类型 | NVARCHAR (Unicode) | VARCHAR |
| 执行计划 | SET SHOWPLAN_TEXT ON | EXPLAIN |
05 · 窗口函数实战
窗口函数是 SQL 里最强大的分析工具,在制造业仪表盘里几乎无处不在。它的核心思想:不对数据分组聚合,而是给每行数据"附加"一个计算结果。
窗口函数 = Excel 里的带范围的公式。比如你在 C2 写 =SUM($B$2:$B$10),然后下拉填充 —— 每行都有一个 SUM 结果,但数据并没有被"压缩"成一行。窗口函数就是 SQL 版的这个操作。
5.1 OEE 排名 — ROW_NUMBER / RANK
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_name | production_date | oee | oee_rank |
|---|---|---|---|
| A线 | 2026-06-09 | 0.92 | 1 |
| B线 | 2026-06-09 | 0.88 | 2 |
| C线 | 2026-06-09 | 0.85 | 3 |
| B线 | 2026-06-08 | 0.91 | 1 |
| A线 | 2026-06-08 | 0.87 | 2 |
| 函数 | 相同值时 | 示例 (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
-- 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_name | downtime_start | prev_end | run_time_between |
|---|---|---|---|
| M01-组装机 | 06-09 10:00 | 06-09 08:30 | 90 |
| M01-组装机 | 06-09 14:00 | 06-09 10:45 | 195 |
| M01-组装机 | 06-10 09:00 | 06-09 15:30 | 1050 |
5.3 累计产量 — SUM OVER
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;
| prod_date | line_name | daily_output | cumulative_output |
|---|---|---|---|
| 2026-06-01 | A线 | 1,200 | 1,200 |
| 2026-06-02 | A线 | 1,350 | 2,550 |
| 2026-06-03 | A线 | 1,100 | 3,650 |
| 2026-06-04 | A线 | 1,280 | 4,930 |
| 2026-06-05 | A线 | 1,400 | 6,330 |
5.4 环比增长 — LAG
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_date | line_name | daily_output | prev_output | mom_rate |
|---|---|---|---|---|
| 2026-06-08 | A线 | 1,350 | 1,200 | +12.5% |
| 2026-06-09 | A线 | 1,100 | 1,350 | -18.5% |
| 2026-06-10 | A线 | 1,280 | 1,100 | +16.4% |
1. 窗口函数只能出现在 SELECT 和 ORDER BY 中,不能在 WHERE 里直接过滤窗口函数的结果。
2. 需要过滤窗口函数结果时,用 CTE 包一层再 WHERE。
3. PARTITION BY 类似 GROUP BY,但不会压缩行数。
06 · 时间范围查询
岗位要求"时间范围过滤",这是制造业仪表盘最高频的操作。你需要掌握各种时间维度的查询。
6.1 今日 / 本周 / 本月 / 本季度
-- ============ 今日产量 ============
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 同比 / 环比
-- 同比:今年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天移动平均)
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_date | line_name | daily_output | ma_7day |
|---|---|---|---|
| 06-04 | A线 | 1,280 | 1,232 |
| 06-05 | A线 | 1,400 | 1,266 |
| 06-06 | A线 | 1,150 | 1,254 |
| 06-07 | A线 | 1,300 | 1,260 |
| 06-08 | A线 | 1,350 | 1,271 |
| 06-09 | A线 | 1,100 | 1,254 |
| 06-10 | A线 | 1,280 | 1,266 |
| SQL 概念 | Excel 公式 |
|---|---|
| 7天移动平均 (窗口函数) | =AVERAGE(B2:B8) 然后下拉填充 |
| PARTITION BY line_id | 按产线分别做,不交叉 |
| ROWS BETWEEN 6 PRECEDING AND CURRENT ROW | 选"当前行和上面6行"的范围 |
6.4 班次查询 (白班 / 夜班)
-- 方法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 查看执行计划
-- 方法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页翻到最后找内容(全表扫描)。有索引 = 先翻目录找到页码,直接翻到那一页。代价是:每加一个索引,插入/更新会变慢(要同时更新目录)。
-- 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 优化检查清单
| # | 优化项 | 说明 |
|---|---|---|
| 1 | 避免 SELECT * | 只取需要的列,减少 I/O 和网络传输 |
| 2 | WHERE 列上不要用函数 | YEAR(date)=2026 → date >= '2026-01-01' |
| 3 | 合理使用索引 | WHERE / JOIN / ORDER BY 字段建索引 |
| 4 | 覆盖索引 | INCLUDE 查询需要的列,避免回表 |
| 5 | 分页查询 | 用 OFFSET ... FETCH 代替取全部 |
| 6 | 避免子查询,改用 JOIN | 子查询通常比 JOIN 慢 |
| 7 | 用 EXISTS 代替 IN (大数据集) | EXISTS 找到就停,IN 会计算全部 |
| 8 | 避免 OR,改用 UNION ALL | OR 可能导致索引失效 |
-- 仪表盘分页加载 (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 里的"自定义视图"或Power Query 的查询。你定义好一个查询逻辑(比如"把产量表和机台表 JOIN 起来,只看最近7天"),保存为一个视图名。以后直接 SELECT * FROM 视图名 就能拿到结果,不用每次写长长的 JOIN。
为什么仪表盘要用视图?
| 好处 | 说明 |
|---|---|
| 简化前端 SQL | 前端只需 SELECT * FROM v_daily_output,不用写复杂 JOIN |
| 统一数据口径 | 多个仪表盘用同一个视图,指标定义一致 |
| 权限控制 | 给前端只开放视图权限,不暴露底层表结构 |
| 维护方便 | 业务逻辑变了,只改视图定义,前端代码不用动 |
创建仪表盘常用视图
-- 视图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 · 面试考点
以下是中际旭创等制造业数据岗位高频出现的数据库面试题。
答题要点:
- 事实表:存业务数据(数值),行数多、增长快。如产量记录、停机记录。
- 维度表:存描述信息(文本),行数少、变化慢。如机台、产线、班次。
- 星型模型:中间一个事实表,通过外键连接多个维度表,形状像星星。
- 优点:JOIN 层级少(只一层),查询性能好,适合 OLAP 分析。
- 与雪花模型对比:雪花模型维度表会再拆子表(如产线表再关联车间表),更规范但 JOIN 更多。
答题要点:
| 类型 | 特点 | 场景 |
|---|---|---|
| 聚集索引 | 决定数据物理排序,每表一个 | 主键、范围查询(如日期) |
| 非聚集索引 | 独立存储,不影响物理排序 | WHERE / JOIN / ORDER BY 字段 |
| 唯一索引 | 保证值不重复 | 产品编码、员工工号 |
| 覆盖索引 | INCLUDE 额外列,避免回表 | 查询只需索引中的列 |
| 复合索引 | 多列组合,遵循最左前缀 | 多条件查询 (line_id + date) |
答题要点:
| 方面 | SQL Server | MySQL |
|---|---|---|
| 开发商 | 微软 | Oracle (开源) |
| 取前N条 | TOP N | LIMIT N |
| 自增 | IDENTITY(1,1) | AUTO_INCREMENT |
| 当前时间 | GETDATE() | NOW() |
| 字符串聚合 | STRING_AGG() | GROUP_CONCAT() |
| 分页 | OFFSET...FETCH | LIMIT...OFFSET |
| 存储过程 | T-SQL,功能强大 | 支持但功能较少 |
| 适用场景 | 企业级 .NET 生态 | Web 应用,LAMP/LEMP |
答题要点(按步骤回答):
- 定位:用 SQL Server Profiler / sys.dm_exec_query_stats 找到慢查询
- 执行计划:看是全表扫描还是索引扫描,找 Table Scan / Clustered Index Scan
- 找瓶颈:WHERE 条件有没有索引?是不是 SELECT *?有没有函数导致索引失效?
- 优化手段:
- 加索引 / 优化索引
- 避免 SELECT *,只取需要的列
- WHERE 列上不用函数
- 子查询改 JOIN
- 大查询拆分 + 分页
- 验证:对比优化前后的执行时间和 IO 读取
答题要点:
| 场景 | 用存储过程 | 用应用层 (Python/Node.js) |
|---|---|---|
| 复杂的数据转换 | ✅ 减少网络传输 | ❌ 数据来回传 |
| 批量数据导入 | ✅ 在数据库内执行快 | ❌ 每条都走网络 |
| 业务逻辑判断 | ❌ 难以调试和测试 | ✅ 代码可测试、可版本控制 |
| ETL 定时任务 | ✅ 配合 SQL Agent | ⚠️ 也可以,但需调度框架 |
| 报表查询 | ⚠️ 视图更灵活 | ✅ ORM 或原生 SQL |
原则:数据密集型操作用存储过程,业务逻辑放应用层。仪表盘场景一般用视图 + 应用层 SQL。
答题要点:
- 问题:每次查询都新建连接 → TCP 握手 + 权限验证 → 慢(几十到几百毫秒)
- 连接池:预先创建一组连接放在池里,用时取出,用完归还,不销毁
- 关键参数:
- 最小连接数 (min pool size) — 启动时创建
- 最大连接数 (max pool size) — 上限,防止打爆数据库
- 连接超时 (connection timeout) — 等不到连接怎么办
- 空闲回收 (idle timeout) — 多久没用的连接关掉
- Node.js 示例:
mssql库自带连接池:new sql.ConnectionPool({max: 10, ...}) - Python 示例:
SQLAlchemy的create_engine(pool_size=10)
答题要点:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 | 最快 |
| Read Committed (默认) | 不会 | 可能 | 可能 | 较快 |
| Repeatable Read | 不会 | 不会 | 可能 | 中等 |
| Serializable | 不会 | 不会 | 不会 | 最慢 |
制造业场景:仪表盘查询通常用 Read Committed(默认)就够了。写入产量数据时,如果并发写入需要一致性,可以用 Repeatable Read。报表查询对实时性要求不高,可以用 NOLOCK 提示提升性能。
答题要点:
- 问题:单表数据量超过千万/亿级 → 查询变慢、写入变慢、备份困难
- 分表 (水平拆分):把一张大表按规则拆成多张小表
- 按时间拆:
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