分类课程智能体AI
文章
订阅
分类课程AI导师
文章
价格
课程进度
1 / 15
下一节把业务装进数据库:建库、建表与准备数据
自在学

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

公网安备湘公网安备43020302000292号 | 湘ICP备2025148919号-1

关于我们隐私政策使用条款

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

公网安备湘公网安备43020302000292号湘ICP备2025148919号-1

编程SQL 实战:小满商店从一张表开始理解数据库与 SQL

从一张表开始理解数据库与 SQL

假设你接手了一家正在长大的网店,名字叫“小满商店”。店铺刚开张时,订单不多,大家用什么记数据都行:客服把客户地址写在聊天置顶里,运营用一张表格登记订单,仓库在另一张表格里改库存,财务再把付款记录抄到自己的文件中。一天十来单时,这套办法似乎没什么问题。

等订单变多,麻烦会一起冒出来。客户换了手机号,客服已经改过,运营的表格里还是旧号码;一笔订单退款后,财务写了“已退款”,运营那里仍显示“已完成”;仓库盘点发现少了两件商品,却说不清是哪次出库造成的。老板问一句“杭州客户上个月买得最多的三种商品是什么”,几个人要花半天时间合并文件,最后还不敢保证答案一致。

这并不是大家不认真,而是数据没有一套共同的结构和规则。同一个客户被重复记录,同一个状态有“已支付”“付款完成”“paid”三种写法,订单与商品靠肉眼对照,修改也没有明确边界。数据库要解决的,正是这类问题:让许多人和许多个程序围绕同一份数据协作,同时尽量保持数据准确、关系清楚、查询可复现。

从散乱记录到结构化数据库

这门课会一直使用“小满商店”作为案例。我们不会一上来背几十个关键字,而是从一个具体问题开始:数据应该怎样放,为什么要拆成表,表之间怎样重新连起来,以及 SQL 为什么只描述“想要什么”,却能让数据库自己决定“怎么找到”。这一章读完,你应该能看懂一张关系图,能说清表、行、列、主键和外键各自解决什么问题,也能写出第一条有业务含义的查询。


先看数据出了什么问题

很多人第一次接触数据库,会把它理解成“更大的 Excel”。这个比喻能帮助我们认识行和列,但只说对了一小部分。表格文件主要解决个人或小团队记录信息的问题;数据库管理系统还要处理结构约束、并发访问、权限、事务、恢复、查询优化等事情。更关键的是,数据库会把“什么数据可以进入系统”写成规则,而不是只依赖填写者记得规范。

先看一份未经整理的订单记录:

订单号客户联系方式商品数量单价付款情况经手人
101苏小满,杭州13800001001白瓷马克杯;原木托盘1;139.90;89.00微信成功 118.90叶舟
102顾言,上海1380000100265W 氮化镓充电器;编织数据线1;1169.00;39.00支付宝成功 188.00叶舟
111苏小满,杭州市13800001001白瓷马克杯;雾蓝中性笔套装;编织数据线1;1;139.90;29.90;39.00微信成功 99.90叶舟

这张表肉眼能读,程序处理起来却有一连串疑问。第一行的“商品”里塞了两个值,“数量”和“单价”也各塞了两个值,三个位置必须按顺序对应。客户“苏小满”出现两次,城市写法不同,手机号也被重复抄写;她以后换号时,旧订单中的副本要不要跟着改?付款情况把支付方式、状态和金额混在一个单元格里。经手人只有姓名,遇到同名员工就无法准确追溯。

数据库真正管理的不是字,而是事实

我们可以把业务记录理解成一组事实:

  • 客户 1 的姓名是苏小满,所在城市是杭州。
  • 商品 1 的名称是白瓷马克杯,当前标价是 39.90 元。
  • 订单 101 属于客户 1,下单时间是 2025-06-18 10:05:00。
  • 订单 101 包含一件商品 1,成交单价是 39.90 元。
  • 付款 1 对应订单 101,金额是 118.90 元,状态是成功。

一条事实里,每个部分的含义都要清楚。“1”本身没有意义,放在 customer_id 这一列里,它才表示客户编号;products.unit_price 表示商品当前标价,order_items.unit_price 表示下单时确认的成交单价,payments.amount 则表示某次付款金额。它们都可能是带两位小数的数值,语义却不同,数据库设计首先要把这些语义分开。

四类常见的数据麻烦

第一类是重复。客户姓名、手机号和城市被抄进每一笔订单,客户下十次单就重复十次。一旦手机号变化,我们得找到所有副本;漏改一个,系统里就会同时存在两个答案。

第二类是歧义。“库存 20”究竟是能卖 20 件、仓库里摆着 20 件,还是扣除锁定量之后剩 20 件?“订单完成”是已付款、已发货,还是客户已签收?如果列的含义没有定义,再精确的数字也可能答非所问。

第三类是失联。订单里只写商品名称,后来商品改名,旧订单还能不能对应到原商品?库存减少了 3,却没有关联商品、操作人和时间,事后就无法还原发生了什么。

第四类是失控。数量写成 -5,付款金额写成“差不多两百”,订单引用了一个不存在的客户,普通表格可能照样保存。数据库可以通过数据类型、非空约束、唯一约束、检查约束和外键约束,在数据进入时就阻止一部分明显错误。

数据库并不会自动理解业务。它只能执行我们写下的结构和规则。字段含义定义错了,约束遗漏了,数据库也可能非常稳定地保存错误数据。所以学 SQL 之前,我们先学会把业务事实说清楚。

从问题反推要记录什么

设计数据时,一个实用方法是先收集未来要回答的问题。小满商店至少会关心这些问题:

  • 某位客户下过哪些订单?
  • 一笔订单里有哪些商品,各买了几件?
  • 商品下单时的价格是多少,折扣是多少?
  • 一笔订单付了几次款,目前是否付清?
  • 哪个分类的销售额更高?
  • 某件商品的库存为什么增加或减少?
  • 哪位员工执行了这次库存变动,他属于哪个部门?

这些问题会反过来决定数据结构。要查询“某位客户的订单”,订单就必须保留稳定的客户标识;要保留历史成交价,订单明细就不能只依赖商品表里随时可能变化的当前价格;要解释库存,除了 products.stock 这个当前数量,还需要逐次记录库存变化。数据库设计不是先画很多框,而是先明确系统需要保留哪些可验证的事实。


关系模型把事实放进表里

关系型数据库最核心的视角很朴素:把同一类事实组织成关系,再用关系运算得到新的结果。在日常 SQL 中,我们通常把关系看成表,把元组看成行,把属性看成列。理论术语有助于理解边界,但写查询时使用“表、行、列”就够了。

“关系”这个词容易让初学者误会。它不只是在说“两张表之间有联系”。一张符合规则的表本身就表达一个关系:customers 表表达“客户编号、姓名、电话、邮箱、城市、注册时间和推荐人之间的对应关系”;表中的每一行,是这个关系中的一条具体记录。

表定义一类对象

来看小满商店的客户表:

customer_idcustomer_namephoneemailcityregistered_atreferrer_id
1苏小满13800001001xiaoman@example.com杭州2024-01-05 09:10:00NULL
2顾言13800001002guyan@example.com上海2024-02-12 14:30:001
3白露NULLbailu@example.com成都2024-06-20 20:15:001
7陆野NULLluye@example.com杭州2025-06-18 10:00:00NULL

表名使用复数 customers,表示它保存客户这类对象。每一列都描述客户的一个属性,每一行都描述一个客户。referrer_id 表示推荐该客户的另一位客户,因此它仍然引用 customers.customer_id。第一位客户没有推荐人,暂时用 NULL 表示这个值未知或不适用。

客户表中表、行、列、单元格与主键的结构

表定义不只是表头。完整定义还会说明每一列使用什么数据类型、能否为空、有没有默认值,以及哪些约束必须成立。例如 customer_id 可以是整数,customer_name 是一定长度内的字符串,registered_at 是日期时间。定义写好之后,“2024-01-05”不会被误当成客户编号,“杭州”也不会被塞进注册时间列。

行是一条完整记录

一行的各列共同描述同一个对象。上表第二行表达:编号 2 的客户叫顾言,来自上海,在指定时间注册,由编号 1 的客户推荐。不能把第二行的姓名和第三行的城市随意拼在一起,否则得到的事实并不存在。

在关系模型里,行的物理位置通常没有业务含义。你看到 1 排在 2 前面,不代表数据库承诺下次查询仍按这个顺序返回。数据库可能因为索引、执行计划、并行处理或数据页变化而用另一种顺序读取。需要稳定顺序时,查询必须明确写 ORDER BY。

sql
SELECT customer_id, customer_name, city
FROM customers
WHERE customer_id IN (1, 2, 3, 7)
ORDER BY customer_id;

结果才明确要求按客户编号从小到大排列:

customer_idcustomer_namecity
1苏小满杭州
2顾言上海
3白露成都
7陆野杭州

列规定一个观察角度

同一列中的值应该表达同一种含义,并使用兼容的数据类型。city 列存城市名称,registered_at 列存注册时间,customer_id 列存客户编号。如果在 city 中偶尔填“杭州 310000”,偶尔填“浙江省杭州市西湖区”,偶尔填“未知”,查询“杭州客户”时就要猜各种写法。

列名应尽量让含义直接可见。name 在单表里看似简洁,连接客户、商品、分类和员工后,会同时出现四个 name,读者很难判断它指谁。课程统一使用 customer_name、product_name、category_name、employee_name,虽然长一点,却能降低查询中的歧义。

单元格应保留一个值

把“白瓷马克杯;原木托盘”放在一个商品单元格里,会让搜索、统计和约束都变难。我们真正想表达的是:一笔订单可以包含多条订单明细,每条明细只指向一种商品。于是原来横向挤在一个单元格里的列表,会变成 order_items 表中的多行。

order_item_idorder_idproduct_idquantityunit_pricediscount_rate
11011139.900.0000
21012189.000.1124

这两行都通过 order_id = 101 指向同一笔订单。商品、数量、价格和折扣一一对应,不再需要用分号的位置猜关系。discount_rate = 0.1124 表示原木托盘按约 11.24% 的比例优惠。以后要增加第三件商品,只需新增一行,不必修改表结构。

把电话号码、标签、商品清单用逗号拼成一个字符串,短期看起来省表,长期会把查询变成拆字符串。只要列表中的每个成员需要单独筛选、统计或关联,就应该认真考虑把它们拆成多行或独立表。

表和查询结果都长得像表

SQL 有一个很实用的特征:查询读取一张或多张表,返回的结果仍然是行和列。结果可以继续被筛选、排序、分组,也可以成为更大查询的一部分。你可以先把它理解成“表进去,表出来”。

例如,我们只保留杭州客户的编号和姓名:

sql
SELECT customer_id, customer_name
FROM customers
WHERE city = '杭州'
ORDER BY customer_id;
customer_idcustomer_name
1苏小满
7陆野

原表有七列,查询结果只有两列;原表有三个客户,结果只有两个。这并没有改动 customers 表,只是根据列选择和行筛选构造了一份结果。


键让每一行可定位,也让表重新连起来

把数据拆成多张表后,一个新问题马上出现:怎样知道订单 101 属于苏小满,而不是另一个同名客户?答案不是把客户姓名复制进订单,而是让两张表共享一个稳定的标识值。

主键回答“到底是哪一行”

主键是能唯一标识一行的一列或一组列。customers.customer_id 是客户表的主键,所以 1 在这张表中最多出现一次,而且主键不能是 NULL。只要拿到客户编号,我们就能无歧义地定位客户。

姓名不适合做客户主键。不同客户可能同名,姓名还可能更改。手机号和邮箱看似唯一,也可能被家庭成员共用、被重新分配或暂时缺失。课程使用系统生成的整数编号作为代理主键,把“识别某行”与容易变化的业务属性分开。

主键不一定只有一列。某些关系天然需要组合键,例如“某学生选了某课程”可以用学生编号与课程编号共同标识。不过“小满商店”的统一表为了便于后续引用,多数使用单列整数主键,例如 order_item_id、payment_id 和 movement_id。

外键回答“这行指向谁”

订单表里的 customer_id 不是订单自身的主键,它引用 customers.customer_id,所以它是外键。外键值相同,意味着这些订单属于同一位客户:

order_idcustomer_idorder_datestatustotal_amount
10112025-06-18 10:05:00已完成118.90
10222025-07-02 21:10:00已完成188.00
11112026-04-01 08:30:00已完成99.90

客户 1 出现两次不是重复客户,而是“一位客户可以有多笔订单”的自然表达。客户信息只在 customers 中保存一次,订单只保存客户编号。需要客户姓名时,SQL 再通过相等的编号把两张表连接起来。

主键、外键与一对多关系

如果外键约束已建立,插入 customer_id = 9999 的订单时,只要客户表中不存在 9999,数据库就会拒绝这条记录。它阻止了“订单声称属于某位客户,但那位客户根本不存在”的孤儿数据。

外键是一列或一组列,外键约束是数据库对这组引用关系执行的规则。表中有一个叫 customer_id 的列,并不自动意味着约束已经存在;建表时仍要明确声明它引用哪张表的哪一列。

一对多不是画出来的,是数据规则形成的

关系图上经常看到“一对一”“一对多”“多对多”。这些词描述的是允许出现多少条相关记录。

customers 到 orders 是一对多:一个客户可以没有订单,也可以有多笔订单;每笔订单只属于一个客户。实现方式是在“多”的一侧,也就是 orders 表中保存 customer_id。

orders 到 products 是多对多:一笔订单能包含多种商品,一种商品也能出现在很多订单里。关系型设计通常用中间表 order_items 把它拆成两个一对多:

  • 一笔订单对应多条订单明细;
  • 一种商品对应多条订单明细。

中间表不只是两枚外键。quantity、unit_price、discount_rate 都是“某商品出现在某订单中”这件事自己的属性。商品表里的 unit_price 是当前标价,订单明细里的 unit_price 是成交时的价格。即使商品明天涨价,昨天订单的金额也不应跟着改变。

自关联仍然是普通的键关系

统一结构中有两处自关联。customers.referrer_id 引用 customers.customer_id,表示老客户推荐新客户;employees.manager_id 引用 employees.employee_id,表示员工的直属负责人。

自关联并不需要一种特殊表。它仍然是外键引用,只是目标表与当前表相同。根客户没有推荐人、最高负责人没有上级时,这两列可以是 NULL。后面学表连接时,我们会给同一张表起两个别名,一边表示“客户”,另一边表示“推荐人”。

唯一键与业务规则

主键负责唯一定位行,但业务上可能还有其他不能重复的属性。例如,系统可能要求已验证邮箱全局唯一,也可能允许同一个手机号关联多个家庭成员。应该把哪一列设为 UNIQUE,取决于真实规则,不能只凭“看起来应该唯一”。

主键与唯一约束也不完全相同。一张表只有一个主键,主键列不能为空;表可以有多个唯一约束,是否允许空值还会受到具体数据库实现和列定义影响。课程后面会把约束写进 CREATE TABLE,这一章先记住:键不是编号的装饰,它把业务中的身份和引用变成数据库能检查的规则。


小满商店用九张表讲完整门课

现在把视野从两张表拉远。小满商店不是为了凑例子随意建表,而是把销售、商品、付款、组织和库存几条业务线放进同一套模型。之后讲筛选、连接、聚合、子查询、事务、索引和高级查询,都会回到这九张表。

小满商店的业务数据地图

客户与销售

customers 保存客户相对稳定的资料:

text
customers(
  customer_id, customer_name, phone, email,
  city, registered_at, referrer_id
)

orders 保存订单头信息。一行代表一笔订单,记录谁在什么时候下单、订单当前状态和订单总额:

text
orders(
  order_id, customer_id, order_date,
  status, total_amount
)

order_items 保存订单明细。一行代表“一笔订单中的一种商品”,数量、成交价和折扣都属于这次购买:

text
order_items(
  order_item_id, order_id, product_id,
  quantity, unit_price, discount_rate
)

payments 保存付款流水。一笔订单可能一次付清,也可能分两次付款,还可能产生失败或退款记录,所以付款不应直接挤进订单的一列:

text
payments(
  payment_id, order_id, paid_at,
  amount, payment_method, status
)

这四张表形成一条很常用的查询路径:客户 → 订单 → 订单明细 → 付款。注意付款连到订单,不直接连客户;某次付款属于哪位客户,可以经由订单推导出来。少存一份可推导的客户编号,就少一个发生矛盾的机会。

商品与分类

categories 保存商品分类,并通过 parent_id 支持父子分类:

text
categories(category_id, category_name, parent_id)

products 保存商品资料、当前标价、当前库存和销售状态:

text
products(
  product_id, category_id, product_name,
  unit_price, stock, status, created_at
)

分类“一对多”关联商品。一个分类可以容纳许多商品,每件商品在当前简化模型中属于一个分类。categories.parent_id 又能把“咖啡器具”放到“厨房用品”之下。以后查询树状分类时,我们会再次遇到自关联。

商品表有 stock,为什么还需要库存流水?stock 让页面快速读取当前数量;流水解释数量怎样到达现在这个值。前者像账户余额,后者像每一笔收支。只保存余额,盘点出现差异时就无法复盘。

组织与库存

departments 保存部门:

text
departments(department_id, department_name)

employees 保存员工、所在部门、直属负责人、岗位、工资和入职日期:

text
employees(
  employee_id, department_id, manager_id,
  employee_name, job_title, salary, hire_date
)

inventory_movements 保存每次入库、出库、退货或盘点调整:

text
inventory_movements(
  movement_id, product_id, employee_id,
  movement_type, quantity, moved_at, note
)

库存流水同时引用商品和员工。于是系统不仅知道“订单 101 让白瓷马克杯减少 1 件”,还知道谁在什么时间执行、属于哪类变动,以及为什么调整。quantity 可以约定入库为正、出库为负,也可以全部存正数,再由 movement_type 决定方向;两种设计都能工作,但全课程必须使用同一套约定。我们会采用“带方向的数量”:入库为正,出库为负,movement_type 说明业务原因。

九张表的关系总览

小满商店九张表的关系总览

起点关系终点外键位置业务含义
customers一对多ordersorders.customer_id客户可以下多笔订单
customers一对多customerscustomers.referrer_id客户可以推荐多位新客户
categories一对多productsproducts.category_id分类下可以有多件商品
categories一对多categoriescategories.parent_id分类可以包含子分类
orders一对多order_itemsorder_items.order_id订单可以有多条明细
products一对多order_itemsorder_items.product_id商品可出现在多笔订单中
orders一对多paymentspayments.order_id订单可以对应多次支付动作
departments一对多employeesemployees.department_id部门可以有多名员工
employees一对多employeesemployees.manager_id负责人可以管理多名员工
products一对多inventory_movementsinventory_movements.product_id商品可以有多次库存变动
employees一对多inventory_movementsinventory_movements.employee_id员工可以处理多次库存变动

为什么不做成一张超级大表

我们当然可以设计一张 all_shop_data,把客户、订单、商品、付款、员工全塞进去。但一笔订单有三件商品和两次付款时,行数会怎样算?如果把商品与付款直接做笛卡尔组合,就可能得到六行,订单总额和客户资料被重复六次。统计时稍不注意,付款金额也会被重复累计。

更麻烦的是修改。员工调部门,需要更新所有历史库存记录中的部门名称;商品改名,需要更新每笔旧订单;客户改城市,需要更新所有订单副本。插入也会受影响:还没产生订单的新商品放不进以订单为中心的大表;删除客户最后一笔订单时,可能顺手把客户资料也删掉。

拆表的目的不是追求表越多越好,而是让一种事实尽量只保存一次,并把不同变化速度的数据分开。客户城市会变,历史订单不会因此改成别人的;商品当前价格会变,历史成交价仍应保留。合理分表后,每次修改的对象和范围更清楚。

“不要重复”不是绝对禁令。订单明细保留成交单价,就是有意保存历史快照;商品表的当前库存和库存流水也可能同时存在。判断一份数据是否该重复,要问它是不是独立的业务事实,以及两份值发生差异时有没有明确解释。


SQL 用声明的方式提出数据问题

模型搭好之后,我们需要一种语言与数据库沟通。SQL 最值得先理解的,不是关键字必须大写,也不是每句末尾有分号,而是它主要采用声明式思维:你描述想要的结果及其条件,数据库负责选择执行步骤。

假设运营想看杭州客户:

sql
SELECT customer_id, customer_name, city
FROM customers
WHERE city = '杭州'
ORDER BY customer_id;

这句话声明了四件事:结果要哪些列,数据来自哪张表,哪些行能留下,结果按什么规则排序。它没有要求数据库“先打开数据文件,从第一行向后找,每发现一个杭州就复制到内存”。数据库可以扫描整张表,也可以使用城市索引;如果统计信息和数据分布变化,它还可以改用另一条执行路径。

声明式 SQL 从业务问题到结果表的过程

把查询读成一句完整的话

SQL 的书写顺序与我们理解它的顺序不完全一样。初学时,可以先按下面的方式读:

先读 FROM customers:这次问题的基础数据来自客户表。

再读 WHERE city = '杭州':只保留城市等于杭州的行。

接着读 SELECT customer_id, customer_name, city:从留下的行中输出这三列。

最后读 ORDER BY customer_id:按客户编号排列最终结果,避免依赖数据库碰巧返回的顺序。

这个阅读顺序以后会扩展到连接、分组和聚合。先找数据来源,再决定行怎样组合和筛选,然后计算分组,最后形成输出。执行器内部可能有更复杂的优化,但逻辑上这样读,比较不容易写乱。

声明式不等于不用考虑性能

“数据库决定怎么执行”并不代表 SQL 写法无所谓。筛选条件能否使用索引、连接列类型是否一致、是否读取了不需要的列、统计信息是否可靠,都会影响优化器可选的路径。我们负责准确描述结果并提供合适的数据结构,优化器在这些条件下寻找执行计划。

可以把它想成导航。你提供目的地、出发点和限制条件,导航选择路线;如果目的地写错,再聪明的导航也到不了正确地方。如果道路数据不完整,路线也可能不理想。后面学习索引和 EXPLAIN 时,我们会看到数据库选了哪条路。

第一条跨表查询

如果要同时显示订单号和客户姓名,单张 orders 表不够,因为它只有 customer_id。我们需要按共同编号连接客户和订单:

sql
SELECT
  o.order_id,
  c.customer_name,
  o.order_date,
  o.status,
  o.total_amount
FROM orders AS o
JOIN customers AS c
  ON c.customer_id = o.customer_id
WHERE o.order_id IN (101, 102, 111)
ORDER BY o.order_id;

使用前面的示例数据,结果是:

order_idcustomer_nameorder_datestatustotal_amount
101苏小满2025-06-18 10:05:00已完成118.90
102顾言2025-07-02 21:10:00已完成188.00
111苏小满2026-04-01 08:30:00已完成99.90

o 和 c 是表别名,分别代表订单表和客户表。ON 后面写连接条件:订单的客户编号与客户表的客户编号相等。SQL 不关心客户行与订单行在磁盘上是否挨着,它只根据值相等建立联系。这也是关系模型与按指针追踪固定路径的一处关键差别。

查询不会默认修改原数据

SELECT 读取并返回结果,不会因为筛选掉某些行就把它们从原表删除。下面的查询只显示有效销售状态的商品:

sql
SELECT product_id, product_name, unit_price, stock
FROM products
WHERE status = '在售'
ORDER BY product_id;

没有出现在结果里的停用商品仍然保存在表中。真正增加、修改和删除数据,需要 INSERT、UPDATE、DELETE。真正改变表结构,则会使用 CREATE、ALTER、DROP。先把“得到一个结果”和“改变数据库状态”分开,能避免不少危险操作。

SQL 大致处理哪些任务

任务常见语句小满商店中的例子
定义结构CREATE、ALTER、DROP创建订单表、增加约束
查询数据SELECT查询杭州客户的订单
写入数据INSERT新增客户、订单和付款
修改数据UPDATE更新订单状态
删除数据DELETE删除误录的测试记录
控制事务START TRANSACTION、COMMIT、ROLLBACK保证下单与扣库存共同成功
管理权限GRANT、REVOKE让财务只能访问所需数据

这些分类帮助我们建立地图,但不必急着背缩写。学到具体业务时再看语句如何协作,比单独记 DDL、DML、DCL 更牢靠。


用 MySQL 8.4 建立课程环境

SQL 有标准,各数据库产品也有自己的实现。课程选择 MySQL 8.4 系列作为练习环境,默认使用 InnoDB 存储引擎与 utf8mb4 字符集。绝大多数关于表、筛选、连接、聚合和事务的思路可以迁移到 PostgreSQL 等关系型数据库,但数据类型、函数、自动编号、标识符规则和部分语法会有差异。

先分清三个东西

MySQL Server 是真正保存数据、检查约束并执行 SQL 的服务进程。mysql 是随 MySQL 提供的命令行客户端,负责把你输入的语句发送给服务器并显示结果。图形化客户端也是客户端,只是用窗口展示对象和结果。

所以,“打开客户端”不等于数据库服务已经运行,“安装了服务”也不等于已经连接成功。遇到问题时先判断是哪一层:服务器没启动、连接参数不对、用户权限不足,还是 SQL 本身有错误。

连接后先做体检

进入 MySQL 客户端后,可以先运行几条不会修改数据的查询:

sql
SELECT VERSION() AS mysql_version;
 
SELECT
  @@character_set_server AS server_charset,
  @@collation_server AS server_collation,
  @@sql_mode AS sql_mode;

结果的具体版本号和模式会随安装配置变化。我们关心三件事:服务确实是 MySQL 8.4 系列,服务器字符集能正确保存中文,SQL 模式包含严格检查。课程创建数据库时会明确指定 utf8mb4,避免依赖机器上的偶然默认值。

你也可以先运行一条完全不依赖业务表的 SQL:

sql
SELECT
  '小满商店' AS shop_name,
  ROUND(39.90 + 89.00 * (1 - 0.1124), 2) AS order_amount;
shop_nameorder_amount
小满商店118.90

这条语句证明客户端、服务器和中文显示已经能协作。MySQL 允许 SELECT 在不写 FROM 的情况下计算表达式。这里只做连通性检查,不代表以后把金额计算都写成常量。

统一几条书写约定

课程示例会把 SQL 关键字写成大写,把表名和列名写成小写蛇形命名,例如 ORDER BY、order_items、registered_at。MySQL 对许多关键字不区分大小写,但统一样式能让结构更容易扫读。

每条完整语句以分号结束。短语句可以写一行,业务查询按子句换行,连接条件再缩进一级。字符串与日期时间使用单引号,数字不加引号。注释使用 -- ,两个连字符后保留一个空格:

sql
SELECT customer_id, customer_name
FROM customers
WHERE city = '杭州'  -- 字符串使用单引号
ORDER BY customer_id;

表名、列名尽量使用英文标识符,展示值可以使用中文。这样既便于跨工具协作,也避免不同系统对中文标识符的转义和大小写处理差异。英文标识符并不妨碍我们用中文理解业务:order_items 就是订单明细,inventory_movements 就是库存变动流水。

错误信息是定位线索

语句写错时,服务器通常会返回错误编号、状态和附近位置。不要只看“报错了”,先从三个方向检查:

  1. 对象是否存在:表名、列名、数据库名有没有拼错;
  2. 语法是否完整:逗号、引号、括号、分号是否成对;
  3. 数据是否符合规则:类型、非空、唯一性、外键引用是否满足。

例如,在业务表还没创建时直接运行:

sql
SELECT * FROM customers;

服务器会告诉你表不存在。这不说明 SELECT 的思想错了,只说明当前数据库里还没有 customers。下一章会从 CREATE DATABASE 和 CREATE TABLE 开始,把本章的概念变成真实结构。

如果 SELECT VERSION() 与中文常量查询都能正常返回,你的基础连接已经可用。先停在这里即可;不要为了“准备充分”提前复制来源不明的整套脚本,后面会逐张建立表并解释每个字段。


初学者最容易混淆的六件事

概念刚接触时,有些说法听起来差不多,写到查询里却会造成完全不同的结果。我们集中把这些边界讲清楚。

数据库不等于数据库管理系统

数据库是按结构组织的数据及相关对象。数据库管理系统是管理这些数据的软件,MySQL 就属于这一层。MySQL 实例中可以管理多个数据库;一个数据库中又可以有多张表、视图、索引等对象。日常交流常把它们都简称“数据库”,遇到安装、权限或架构问题时必须分清。

表不等于一份任意格式的表格

关系表有列定义、数据类型和约束,同一列应表达一致的属性。合并单元格、把颜色当状态、在标题上方写几行备注,这些在人看的表格里很常见,却不是关系表的数据表达方式。数据库中的样式由应用负责,表保存可计算的事实。

主键不等于行号

表格软件左侧的第 2 行、第 3 行只是显示位置;插入、排序后位置会变化。主键是数据的一部分,应该稳定地标识同一业务对象。customer_id = 1 无论显示在结果的第几行,都仍指向同一位客户。

外键不等于“名字相同的列”

orders.customer_id 与 customers.customer_id 名字相同,能提示它们可能有关联,但真正的引用完整性来自外键约束。反过来,列名不同也可以建立外键,只要类型兼容、目标列满足被引用条件,并在定义中明确写出关系。

NULL 不等于空字符串或数字零

NULL 表示值缺失、未知或不适用;'' 是长度为零的已知字符串;0 是明确的数字。没有推荐人时 referrer_id 可以是 NULL,库存为 0 则表示明确知道没有可用库存。后面学习筛选时会看到,NULL 不能用普通等号判断。

结果顺序不等于存储顺序

即使连续十次查询都按主键顺序返回,只要没写 ORDER BY,这个顺序就不是查询承诺的一部分。数据量、索引和执行计划一变,结果可能重排。只在展示或业务逻辑确实需要顺序时写排序,并明确用哪些列处理并列情况。

SQL 能查到不等于数据模型合理

把所有信息塞进一个文本列,也能用字符串函数勉强查;没有外键,也能手工连接;价格全部存成字符,也能临时转换。这些写法的问题会在数据增长、多人修改和需求变化时放大。SQL 的强大不能替代清楚的模型,查询技巧也不该用来长期弥补结构混乱。


用一条业务问题检查你的理解

我们用一个稍完整的问题收尾:找出杭州客户状态为已支付、已发货或已完成的订单,并显示订单号、客户姓名、日期和总额。

sql
SELECT
  o.order_id,
  c.customer_name,
  o.order_date,
  o.total_amount
FROM customers AS c
JOIN orders AS o
  ON o.customer_id = c.customer_id
WHERE c.city = '杭州'
  AND o.status IN ('已支付', '已发货', '已完成')
ORDER BY o.order_date, o.order_id;
order_idcustomer_nameorder_datetotal_amount
101苏小满2025-06-18 10:05:00118.90
111苏小满2026-04-01 08:30:0099.90

这条查询已经包含本章的主线:customers 和 orders 各保存一种事实,主键与外键提供连接条件,FROM 和 JOIN 确定数据来源,WHERE 声明筛选规则,SELECT 决定输出列,ORDER BY 承诺结果顺序。你不需要现在记住每个关键字的全部细节,只要能顺着业务意思读懂它。

练习:辨认表、行、列和键

1
在 customers 表中,哪一项最适合稳定地标识某一位客户?
2
下列哪些列在统一模型中属于外键?
3
只要 SELECT 查询连续多次都按主键顺序返回,就可以认为数据库保证了这个顺序。

练习:把重复数据拆开

小满商店曾用一张表记录“订单号、客户姓名、客户电话、商品 1、商品 2、商品 3、付款金额”。请回答两个问题:

  1. 当一笔订单有四件商品时,这张表会遇到什么结构问题?
  2. 用本章九张表中的哪些表,可以更自然地表达客户、订单、商品与付款?

固定的“商品 1、商品 2、商品 3”把商品数量写进了表结构。出现第四件商品就要新增列,而且每个商品还需要对应数量、成交价和折扣,列会不断膨胀。更自然的做法是:客户写入 customers,订单头写入 orders,每种商品各写成 order_items 的一行,商品资料放在 products,付款动作写入 payments。这样订单增加商品时只增加明细行,不需要修改表结构。

练习:读懂一条查询

下面的查询要回答什么问题?如果去掉 ORDER BY,结果内容和结果顺序分别会发生什么变化?

sql
SELECT product_id, product_name, stock
FROM products
WHERE status = '在售'
  AND stock < 40
ORDER BY stock, product_id;

它找出正在销售且当前库存少于 40 的商品,显示商品编号、名称和库存。固定数据中会留下暖光阅读灯、65W 氮化镓充电器和保温随行杯。结果先按库存从小到大排列,库存相同时再按商品编号排列。去掉 ORDER BY 后,符合条件的行集合不变,但返回顺序不再有保证。

动手前的模型检查

在进入下一章之前,试着不看前文回答下面五句话:

  • 一行表示什么?
  • 一列为什么不能混放不同含义?
  • 主键与外键分别解决什么问题?
  • 为什么订单与商品之间需要 order_items?
  • 为什么查询需要稳定顺序时必须写 ORDER BY?

如果你能用自己的话说清楚,就已经抓住了关系型数据库的骨架。下一步不是再加一批术语,而是把这套骨架写成 MySQL 能执行的定义:创建 xiaoman_shop 数据库,为九张表选择数据类型,声明主键、外键与其他约束,再放入第一批可供后续章节查询的数据。

  • 先看数据出了什么问题
    • 数据库真正管理的不是字,而是事实
    • 四类常见的数据麻烦
    • 从问题反推要记录什么
  • 关系模型把事实放进表里
    • 表定义一类对象
    • 行是一条完整记录
    • 列规定一个观察角度
    • 单元格应保留一个值
    • 表和查询结果都长得像表
  • 键让每一行可定位,也让表重新连起来
    • 主键回答“到底是哪一行”
    • 外键回答“这行指向谁”
    • 一对多不是画出来的,是数据规则形成的
    • 自关联仍然是普通的键关系
    • 唯一键与业务规则
  • 小满商店用九张表讲完整门课
    • 客户与销售
    • 商品与分类
    • 组织与库存
    • 九张表的关系总览
    • 为什么不做成一张超级大表
  • SQL 用声明的方式提出数据问题
    • 把查询读成一句完整的话
    • 声明式不等于不用考虑性能
    • 第一条跨表查询
    • 查询不会默认修改原数据
    • SQL 大致处理哪些任务
  • 用 MySQL 8.4 建立课程环境
    • 先分清三个东西
    • 连接后先做体检
    • 统一几条书写约定
    • 错误信息是定位线索
  • 初学者最容易混淆的六件事
    • 数据库不等于数据库管理系统
    • 表不等于一份任意格式的表格
    • 主键不等于行号
    • 外键不等于“名字相同的列”
    • NULL 不等于空字符串或数字零
    • 结果顺序不等于存储顺序
    • SQL 能查到不等于数据模型合理
  • 用一条业务问题检查你的理解
    • 练习:辨认表、行、列和键
    • 练习:把重复数据拆开
    • 练习:读懂一条查询
    • 动手前的模型检查

目录

  • 先看数据出了什么问题
    • 数据库真正管理的不是字,而是事实
    • 四类常见的数据麻烦
    • 从问题反推要记录什么
  • 关系模型把事实放进表里
    • 表定义一类对象
    • 行是一条完整记录
    • 列规定一个观察角度
    • 单元格应保留一个值
    • 表和查询结果都长得像表
  • 键让每一行可定位,也让表重新连起来
    • 主键回答“到底是哪一行”
    • 外键回答“这行指向谁”
    • 一对多不是画出来的,是数据规则形成的
    • 自关联仍然是普通的键关系
    • 唯一键与业务规则
  • 小满商店用九张表讲完整门课
    • 客户与销售
    • 商品与分类
    • 组织与库存
    • 九张表的关系总览
    • 为什么不做成一张超级大表
  • SQL 用声明的方式提出数据问题
    • 把查询读成一句完整的话
    • 声明式不等于不用考虑性能
    • 第一条跨表查询
    • 查询不会默认修改原数据
    • SQL 大致处理哪些任务
  • 用 MySQL 8.4 建立课程环境
    • 先分清三个东西
    • 连接后先做体检
    • 统一几条书写约定
    • 错误信息是定位线索
  • 初学者最容易混淆的六件事
    • 数据库不等于数据库管理系统
    • 表不等于一份任意格式的表格
    • 主键不等于行号
    • 外键不等于“名字相同的列”
    • NULL 不等于空字符串或数字零
    • 结果顺序不等于存储顺序
    • SQL 能查到不等于数据模型合理
  • 用一条业务问题检查你的理解
    • 练习:辨认表、行、列和键
    • 练习:把重复数据拆开
    • 练习:读懂一条查询
    • 动手前的模型检查