Skip to content

SQL 基础与表设计

Phase 04 — Database 涵盖:SQL · Table · Primary Key · Foreign Key · Index · JOIN · Transaction


1. 学习目标

完成本知识点后,你应该能够:

  • 理解关系型数据库的核心概念:表、行、列、主键、外键
  • 编写基础 SQL:SELECTINSERTUPDATEDELETE
  • 使用 JOIN 查询关联表数据
  • 理解索引的作用与适用场景
  • 使用事务保证多步操作的原子性
  • 为数字孪生场景设计设备与遥测数据的表结构

2. 为什么需要

数字孪生后端需要持久化存储:设备档案、历史轨迹、告警记录、工单状态……这些数据不能只在内存里——服务重启就会丢失。

关系型数据库(以 PostgreSQL 为代表)用 SQL 作为标准查询语言,通过组织结构化数据,通过主键 / 外键表达实体关系,通过事务保证数据一致性。这是后端开发中最基础、最通用的数据持久化方案。


3. 核心概念

3.1 SQL(Structured Query Language)

操作关系型数据库的标准语言,分为四类:

类型关键字作用
DDLCREATE / ALTER / DROP定义表结构
DMLSELECT / INSERT / UPDATE / DELETE增删改查
DCLGRANT / REVOKE权限管理
TCLBEGIN / COMMIT / ROLLBACK事务控制

3.2 Table(表)

表是数据的二维结构:每行(row) 是一条记录,每列(column) 是一个字段。

sql
CREATE TABLE devices (
    id          VARCHAR(32) PRIMARY KEY,
    name        VARCHAR(100) NOT NULL,
    device_type VARCHAR(50)  NOT NULL,
    status      VARCHAR(20)  DEFAULT 'idle',
    created_at  TIMESTAMPTZ  DEFAULT NOW()
);

3.3 Primary Key(主键)

唯一标识一行记录的列(或列组合):

  • 不能为 NULL
  • 值必须唯一
  • 每张表通常有一个主键
sql
-- 单字段主键
id VARCHAR(32) PRIMARY KEY

-- 自增整数主键(PostgreSQL 用 SERIAL 或 GENERATED)
id SERIAL PRIMARY KEY

3.4 Foreign Key(外键)

引用另一张表主键的列,建立表间关联:

sql
CREATE TABLE telemetry (
    id         BIGSERIAL PRIMARY KEY,
    device_id  VARCHAR(32) NOT NULL REFERENCES devices(id),
    x          DOUBLE PRECISION NOT NULL,
    y          DOUBLE PRECISION NOT NULL,
    recorded_at TIMESTAMPTZ DEFAULT NOW()
);

device_id 必须是 devices.id 中已存在的值,否则插入失败——这保证了引用完整性

3.5 Index(索引)

加速查询的数据结构,类似书的目录:

sql
-- 为高频查询字段建索引
CREATE INDEX idx_telemetry_device_time
    ON telemetry (device_id, recorded_at DESC);
场景是否建索引
WHERE / JOIN 常用列
唯一约束列自动有索引
小表、低频写入通常不需要
频繁 UPDATE 的列谨慎,索引有维护成本

3.6 JOIN(表连接)

将多张表的数据合并查询:

JOIN 类型说明
INNER JOIN只返回两表都匹配的行
LEFT JOIN左表全保留,右表无匹配则为 NULL
RIGHT JOIN右表全保留
FULL JOIN两表全保留
sql
SELECT d.id, d.name, t.x, t.y, t.recorded_at
FROM devices d
INNER JOIN telemetry t ON d.id = t.device_id
WHERE d.status = 'moving'
ORDER BY t.recorded_at DESC
LIMIT 10;

3.7 Transaction(事务)

一组 SQL 操作要么全部成功,要么全部回滚:

sql
BEGIN;

UPDATE devices SET status = 'maintenance' WHERE id = 'AGV-001';
INSERT INTO maintenance_logs (device_id, reason) VALUES ('AGV-001', 'scheduled check');

COMMIT;   -- 全部生效
-- ROLLBACK;  -- 全部撤销

事务遵循 ACID

属性含义
Atomicity原子性,全成功或全失败
Consistency一致性,约束始终满足
Isolation隔离性,并发事务互不干扰
Durability持久性,提交后数据不丢

4. 基础语法

4.1 数字孪生表结构设计

sql
-- 设备表
CREATE TABLE devices (
    id          VARCHAR(32) PRIMARY KEY,
    name        VARCHAR(100) NOT NULL,
    device_type VARCHAR(50)  NOT NULL,  -- agv, sensor, robot_arm
    status      VARCHAR(20)  DEFAULT 'idle',
    created_at  TIMESTAMPTZ  DEFAULT NOW()
);

-- 遥测数据表(外键关联设备)
CREATE TABLE telemetry (
    id          BIGSERIAL PRIMARY KEY,
    device_id   VARCHAR(32) NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
    x           DOUBLE PRECISION NOT NULL,
    y           DOUBLE PRECISION NOT NULL,
    speed       DOUBLE PRECISION DEFAULT 0,
    battery     INT CHECK (battery >= 0 AND battery <= 100),
    recorded_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_telemetry_device_time ON telemetry (device_id, recorded_at DESC);

4.2 CRUD 操作

sql
-- 插入
INSERT INTO devices (id, name, device_type, status)
VALUES ('AGV-001', 'Alpha AGV', 'agv', 'idle');

INSERT INTO telemetry (device_id, x, y, speed, battery)
VALUES ('AGV-001', 12.5, 3.8, 1.2, 85);

-- 查询
SELECT * FROM devices WHERE device_type = 'agv';
SELECT id, name, status FROM devices WHERE status = 'moving';

-- 更新
UPDATE devices SET status = 'moving' WHERE id = 'AGV-001';

-- 删除
DELETE FROM telemetry WHERE recorded_at < NOW() - INTERVAL '30 days';

4.3 JOIN 查询

sql
-- 查询每台 AGV 的最新位置
SELECT DISTINCT ON (d.id)
    d.id, d.name, d.status, t.x, t.y, t.battery, t.recorded_at
FROM devices d
LEFT JOIN telemetry t ON d.id = t.device_id
WHERE d.device_type = 'agv'
ORDER BY d.id, t.recorded_at DESC;

4.4 事务示例

sql
BEGIN;

-- 设备进入维护状态,同时记录日志
UPDATE devices SET status = 'maintenance' WHERE id = 'AGV-002';

INSERT INTO maintenance_logs (device_id, reason, started_at)
VALUES ('AGV-002', 'battery replacement', NOW());

-- 若任一步失败,执行 ROLLBACK
COMMIT;

5. 代码解析

sql
device_id VARCHAR(32) NOT NULL REFERENCES devices(id) ON DELETE CASCADE
  • NOT NULL:该列不能为空
  • REFERENCES devices(id):外键,值必须存在于 devices.id
  • ON DELETE CASCADE:删除设备时,关联的遥测记录一并删除
sql
CREATE INDEX idx_telemetry_device_time ON telemetry (device_id, recorded_at DESC);

复合索引 (device_id, recorded_at DESC) 适合「按设备查最近 N 条遥测」这类查询,PostgreSQL 可以利用索引避免全表扫描。

sql
SELECT DISTINCT ON (d.id) ...
ORDER BY d.id, t.recorded_at DESC;

PostgreSQL 特有的 DISTINCT ON:每组(这里是每个 d.id)只取排序后的第一行,即最新一条遥测。


6. JavaScript / TypeScript 对比

概念SQL / 关系型数据库JavaScript / 前端
数据存储持久化表,服务端内存对象、localStorage(有限)
查询语言声明式 SQL命令式 filter/map/reduce
关联数据JOIN 多表手动嵌套对象或多次请求
事务原生支持 ACID无(IndexedDB 有部分支持)
Schema强类型,先定义表结构动态,对象随意增删字段
并发安全数据库引擎处理单线程,通常不涉及

关键差异

  1. SQL 是声明式的——你描述「要什么数据」,数据库决定怎么取
  2. 关系型数据库强调范式约束,数据完整性由数据库保证
  3. 前端 Array.filter() 类比 WHERE,但 SQL 在服务端处理百万行更高效

7. 常见错误

错误 1:忘记主键

sql
-- ❌ 无唯一标识,难以更新/删除特定行
CREATE TABLE devices (name VARCHAR(100));

-- ✅ 每表应有主键
CREATE TABLE devices (id VARCHAR(32) PRIMARY KEY, name VARCHAR(100));

错误 2:外键引用不存在的记录

sql
-- ❌ AGV-999 不存在于 devices 表
INSERT INTO telemetry (device_id, x, y) VALUES ('AGV-999', 1.0, 2.0);
-- ERROR: insert or update on table "telemetry" violates foreign key constraint

错误 3:SELECT * 在生产环境滥用

sql
-- ❌ 返回所有列,含不需要的大字段
SELECT * FROM telemetry WHERE device_id = 'AGV-001';

-- ✅ 只取需要的列
SELECT x, y, speed, recorded_at FROM telemetry WHERE device_id = 'AGV-001';

错误 4:无索引的大表查询

sql
-- ❌ 百万行全表扫描
SELECT * FROM telemetry WHERE device_id = 'AGV-001' ORDER BY recorded_at DESC;

-- ✅ 在 (device_id, recorded_at) 上建索引

错误 5:事务未正确处理失败

sql
BEGIN;
UPDATE devices SET status = 'error' WHERE id = 'AGV-001';
-- 中间某步失败但未 ROLLBACK,可能导致部分提交
COMMIT;

在 Go 中应使用 tx.Rollback() 配合 defer 处理(见 Phase 04 下一文档)。


8. 实际应用

数字孪生 — 设备数据持久化

Three.js 前端展示实时 AGV 位置,后端需要:

  1. devices 表:设备档案(ID、类型、状态)
  2. telemetry 表:历史轨迹(坐标、速度、电量、时间戳)
  3. alerts 表:告警记录(外键关联设备)

典型查询场景:

场景SQL 模式
设备列表页SELECT * FROM devices WHERE device_type = 'agv'
实时位置(最新一条)DISTINCT ON + ORDER BY recorded_at DESC
历史轨迹回放WHERE device_id = ? AND recorded_at BETWEEN ? AND ?
低电量告警统计JOIN devices + 子查询过滤 battery < 20

数据流:

AGV 上报 → Go Handler → INSERT telemetry → PostgreSQL 持久化

                          Three.js 查询历史 / WebSocket 推送实时

9. 深入理解

9.1 范式简述

范式要求数字孪生示例
1NF列不可再分坐标拆成 xy 两列,而非 "12.5,3.8" 字符串
2NF非主键列完全依赖主键遥测表的 speed 依赖 telemetry.id,而非部分依赖
3NF消除传递依赖设备类型名不冗余存储在遥测表,通过 device_id 关联

实际项目中不必追求最高范式,适度冗余换查询性能是常见做法。

9.2 索引类型(PostgreSQL)

  • B-tree(默认):等值、范围查询
  • Hash:仅等值查询
  • GIN / GiST:全文搜索、地理数据

9.3 隔离级别

PostgreSQL 默认 READ COMMITTED。高并发场景需了解:

级别脏读不可重复读幻读
READ COMMITTED
REPEATABLE READ否(PG 实现)
SERIALIZABLE

9.4 JOIN vs 应用层组装

  • 数据量小、关联简单:JOIN 一次取出,减少往返
  • 微服务拆分后:可能无法跨库 JOIN,改在应用层组装

10. 练习

请独立完成,不要查看答案。完成后说「检查答案」并提交你的 SQL。

Level 1 — 基础

练习 1.1:编写 CREATE TABLE 语句,创建 warehouses 表,包含 id(主键)、namewidthheight

练习 1.2:向 devices 表插入 3 台 AGV 记录(自行定义 ID 和名称)。

练习 1.3:编写 SELECT 查询所有 status = 'idle' 的设备。

练习 1.4:编写 UPDATEAGV-001 的状态改为 'moving'

Level 2 — 应用

练习 2.1:创建 alerts 表,包含 id(自增主键)、device_id(外键引用 devices)、levelwarning/critical)、messagecreated_at

练习 2.2:编写 INNER JOIN 查询,返回告警及其对应设备名称。

练习 2.3:为 telemetry 表添加索引,优化「按设备 ID 查最近数据」的查询。

练习 2.4:用事务实现:设备状态改为 error,同时插入一条 critical 级别告警。

Level 3 — 综合

练习 3.1:设计并创建完整的数字孪生 schema:warehousesdevices(含 warehouse_id 外键)、telemetryalerts 四表。

练习 3.2:编写查询:返回每个仓库中 battery < 20 的 AGV 列表(含仓库名、设备名、最新电量)。

练习 3.3:编写查询:统计每台设备过去 24 小时的遥测记录数,按记录数降序排列。

Level 4 — 项目实践

练习 4.1:在本地 PostgreSQL 中执行完整 schema 搭建,插入至少 5 台设备、20 条遥测、3 条告警,编写 5 个业务查询(设备列表、最新位置、历史轨迹、低电量设备、告警列表)并验证结果。


11. 学习检查

完成练习后,确认你能回答:

  1. 主键和外键分别解决什么问题?
  2. INNER JOINLEFT JOIN 有什么区别?
  3. 什么情况下应该建索引?什么情况下不该建?
  4. 事务的 ACID 四个属性分别是什么意思?
  5. 为什么数字孪生场景需要 telemetry 独立表而非把所有字段放 devices
  6. ON DELETE CASCADE 的行为是什么?

12. 下一步

已完成下一知识点关系
SQL, Table, PK, FK, Index, JOIN, TransactionPostgreSQL在真实数据库中执行 SQL
database/sql用 Go 代码操作数据库
Connection Pool生产环境连接管理

建议顺序:先在 PostgreSQL 中手动执行本节的 SQL,再学习 Go 的 database/sql 包进行程序化访问。


前置知识提醒

  • 已完成 Phase 03(Go Web / REST API),能编写 Handler 处理 HTTP 请求
  • 本地或 Docker 中已安装 PostgreSQL(下一文档会详细说明)
  • 可使用 psql 或 pgAdmin 执行 SQL

学习导航

上一篇:Gin、JWT、API Design · 对应练习 · 下一篇:PostgreSQL 与 database/sql