假设你接手了一家正在长大的网店,名字叫“小满商店”。店铺刚开张时,订单不多,大家用什么记数据都行:客服把客户地址写在聊天置顶里,运营用一张表格登记订单,仓库在另一张表格里改库存,财务再把付款记录抄到自己的文件中。一天十来单时,这套办法似乎没什么问题。
等订单变多,麻烦会一起冒出来。客户换了手机号,客服已经改过,运营的表格里还是旧号码;一笔订单退款后,财务写了“已退款”,运营那里仍显示“已完成”;仓库盘点发现少了两件商品,却说不清是哪次出库造成的。老板问一句“杭州客户上个月买得最多的三种商品是什么”,几个人要花半天时间合并文件,最后还不敢保证答案一致。
这并不是大家不认真,而是数据没有一套共同的结构和规则。同一个客户被重复记录,同一个状态有“已支付”“付款完成”“paid”三种写法,订单与商品靠肉眼对照,修改也没有明确边界。数据库要解决的,正是这类问题:让许多人和许多个程序围绕同一份数据协作,同时尽量保持数据准确、关系清楚、查询可复现。

这门课会一直使用“小满商店”作为案例。我们不会一上来背几十个关键字,而是从一个具体问题开始:数据应该怎样放,为什么要拆成表,表之间怎样重新连起来,以及 SQL 为什么只描述“想要什么”,却能让数据库自己决定“怎么找到”。这一章读完,你应该能看懂一张关系图,能说清表、行、列、主键和外键各自解决什么问题,也能写出第一条有业务含义的查询。
很多人第一次接触数据库,会把它理解成“更大的 Excel”。这个比喻能帮助我们认识行和列,但只说对了一小部分。表格文件主要解决个人或小团队记录信息的问题;数据库管理系统还要处理结构约束、并发访问、权限、事务、恢复、查询优化等事情。更关键的是,数据库会把“什么数据可以进入系统”写成规则,而不是只依赖填写者记得规范。
先看一份未经整理的订单记录:
这张表肉眼能读,程序处理起来却有一连串疑问。第一行的“商品”里塞了两个值,“数量”和“单价”也各塞了两个值,三个位置必须按顺序对应。客户“苏小满”出现两次,城市写法不同,手机号也被重复抄写;她以后换号时,旧订单中的副本要不要跟着改?付款情况把支付方式、状态和金额混在一个单元格里。经手人只有姓名,遇到同名员工就无法准确追溯。
我们可以把业务记录理解成一组事实:
一条事实里,每个部分的含义都要清楚。“1”本身没有意义,放在 customer_id 这一列里,它才表示客户编号;products.unit_price 表示商品当前标价,order_items.unit_price 表示下单时确认的成交单价,payments.amount 则表示某次付款金额。它们都可能是带两位小数的数值,语义却不同,数据库设计首先要把这些语义分开。
第一类是重复。客户姓名、手机号和城市被抄进每一笔订单,客户下十次单就重复十次。一旦手机号变化,我们得找到所有副本;漏改一个,系统里就会同时存在两个答案。
第二类是歧义。“库存 20”究竟是能卖 20 件、仓库里摆着 20 件,还是扣除锁定量之后剩 20 件?“订单完成”是已付款、已发货,还是客户已签收?如果列的含义没有定义,再精确的数字也可能答非所问。
第三类是失联。订单里只写商品名称,后来商品改名,旧订单还能不能对应到原商品?库存减少了 3,却没有关联商品、操作人和时间,事后就无法还原发生了什么。
第四类是失控。数量写成 -5,付款金额写成“差不多两百”,订单引用了一个不存在的客户,普通表格可能照样保存。数据库可以通过数据类型、非空约束、唯一约束、检查约束和外键约束,在数据进入时就阻止一部分明显错误。
数据库并不会自动理解业务。它只能执行我们写下的结构和规则。字段含义定义错了,约束遗漏了,数据库也可能非常稳定地保存错误数据。所以学 SQL 之前,我们先学会把业务事实说清楚。
设计数据时,一个实用方法是先收集未来要回答的问题。小满商店至少会关心这些问题:
这些问题会反过来决定数据结构。要查询“某位客户的订单”,订单就必须保留稳定的客户标识;要保留历史成交价,订单明细就不能只依赖商品表里随时可能变化的当前价格;要解释库存,除了 products.stock 这个当前数量,还需要逐次记录库存变化。数据库设计不是先画很多框,而是先明确系统需要保留哪些可验证的事实。
关系型数据库最核心的视角很朴素:把同一类事实组织成关系,再用关系运算得到新的结果。在日常 SQL 中,我们通常把关系看成表,把元组看成行,把属性看成列。理论术语有助于理解边界,但写查询时使用“表、行、列”就够了。
“关系”这个词容易让初学者误会。它不只是在说“两张表之间有联系”。一张符合规则的表本身就表达一个关系:customers 表表达“客户编号、姓名、电话、邮箱、城市、注册时间和推荐人之间的对应关系”;表中的每一行,是这个关系中的一条具体记录。
来看小满商店的客户表:
表名使用复数 customers,表示它保存客户这类对象。每一列都描述客户的一个属性,每一行都描述一个客户。referrer_id 表示推荐该客户的另一位客户,因此它仍然引用 customers.customer_id。第一位客户没有推荐人,暂时用 NULL 表示这个值未知或不适用。

表定义不只是表头。完整定义还会说明每一列使用什么数据类型、能否为空、有没有默认值,以及哪些约束必须成立。例如 customer_id 可以是整数,customer_name 是一定长度内的字符串,registered_at 是日期时间。定义写好之后,“2024-01-05”不会被误当成客户编号,“杭州”也不会被塞进注册时间列。
一行的各列共同描述同一个对象。上表第二行表达:编号 2 的客户叫顾言,来自上海,在指定时间注册,由编号 1 的客户推荐。不能把第二行的姓名和第三行的城市随意拼在一起,否则得到的事实并不存在。
在关系模型里,行的物理位置通常没有业务含义。你看到 1 排在 2 前面,不代表数据库承诺下次查询仍按这个顺序返回。数据库可能因为索引、执行计划、并行处理或数据页变化而用另一种顺序读取。需要稳定顺序时,查询必须明确写 ORDER BY。
SELECT customer_id, customer_name, city
FROM customers
WHERE customer_id IN (1, 2, 3, 7)
ORDER BY customer_id;结果才明确要求按客户编号从小到大排列:
同一列中的值应该表达同一种含义,并使用兼容的数据类型。city 列存城市名称,registered_at 列存注册时间,customer_id 列存客户编号。如果在 city 中偶尔填“杭州 310000”,偶尔填“浙江省杭州市西湖区”,偶尔填“未知”,查询“杭州客户”时就要猜各种写法。
列名应尽量让含义直接可见。name 在单表里看似简洁,连接客户、商品、分类和员工后,会同时出现四个 name,读者很难判断它指谁。课程统一使用 customer_name、product_name、category_name、employee_name,虽然长一点,却能降低查询中的歧义。
把“白瓷马克杯;原木托盘”放在一个商品单元格里,会让搜索、统计和约束都变难。我们真正想表达的是:一笔订单可以包含多条订单明细,每条明细只指向一种商品。于是原来横向挤在一个单元格里的列表,会变成 order_items 表中的多行。
这两行都通过 order_id = 101 指向同一笔订单。商品、数量、价格和折扣一一对应,不再需要用分号的位置猜关系。discount_rate = 0.1124 表示原木托盘按约 11.24% 的比例优惠。以后要增加第三件商品,只需新增一行,不必修改表结构。
把电话号码、标签、商品清单用逗号拼成一个字符串,短期看起来省表,长期会把查询变成拆字符串。只要列表中的每个成员需要单独筛选、统计或关联,就应该认真考虑把它们拆成多行或独立表。
SQL 有一个很实用的特征:查询读取一张或多张表,返回的结果仍然是行和列。结果可以继续被筛选、排序、分组,也可以成为更大查询的一部分。你可以先把它理解成“表进去,表出来”。
例如,我们只保留杭州客户的编号和姓名:
SELECT customer_id, customer_name
FROM customers
WHERE city = '杭州'
ORDER BY customer_id;原表有七列,查询结果只有两列;原表有三个客户,结果只有两个。这并没有改动 customers 表,只是根据列选择和行筛选构造了一份结果。
把数据拆成多张表后,一个新问题马上出现:怎样知道订单 101 属于苏小满,而不是另一个同名客户?答案不是把客户姓名复制进订单,而是让两张表共享一个稳定的标识值。
主键是能唯一标识一行的一列或一组列。customers.customer_id 是客户表的主键,所以 1 在这张表中最多出现一次,而且主键不能是 NULL。只要拿到客户编号,我们就能无歧义地定位客户。
姓名不适合做客户主键。不同客户可能同名,姓名还可能更改。手机号和邮箱看似唯一,也可能被家庭成员共用、被重新分配或暂时缺失。课程使用系统生成的整数编号作为代理主键,把“识别某行”与容易变化的业务属性分开。
主键不一定只有一列。某些关系天然需要组合键,例如“某学生选了某课程”可以用学生编号与课程编号共同标识。不过“小满商店”的统一表为了便于后续引用,多数使用单列整数主键,例如 order_item_id、payment_id 和 movement_id。
订单表里的 customer_id 不是订单自身的主键,它引用 customers.customer_id,所以它是外键。外键值相同,意味着这些订单属于同一位客户:
客户 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 保存客户相对稳定的资料:
customers(
customer_id, customer_name, phone, email,
city, registered_at, referrer_id
)orders 保存订单头信息。一行代表一笔订单,记录谁在什么时候下单、订单当前状态和订单总额:
orders(
order_id, customer_id, order_date,
status, total_amount
)order_items 保存订单明细。一行代表“一笔订单中的一种商品”,数量、成交价和折扣都属于这次购买:
order_items(
order_item_id, order_id, product_id,
quantity, unit_price, discount_rate
)payments 保存付款流水。一笔订单可能一次付清,也可能分两次付款,还可能产生失败或退款记录,所以付款不应直接挤进订单的一列:
payments(
payment_id, order_id, paid_at,
amount, payment_method, status
)这四张表形成一条很常用的查询路径:客户 → 订单 → 订单明细 → 付款。注意付款连到订单,不直接连客户;某次付款属于哪位客户,可以经由订单推导出来。少存一份可推导的客户编号,就少一个发生矛盾的机会。
categories 保存商品分类,并通过 parent_id 支持父子分类:
categories(category_id, category_name, parent_id)products 保存商品资料、当前标价、当前库存和销售状态:
products(
product_id, category_id, product_name,
unit_price, stock, status, created_at
)分类“一对多”关联商品。一个分类可以容纳许多商品,每件商品在当前简化模型中属于一个分类。categories.parent_id 又能把“咖啡器具”放到“厨房用品”之下。以后查询树状分类时,我们会再次遇到自关联。
商品表有 stock,为什么还需要库存流水?stock 让页面快速读取当前数量;流水解释数量怎样到达现在这个值。前者像账户余额,后者像每一笔收支。只保存余额,盘点出现差异时就无法复盘。
departments 保存部门:
departments(department_id, department_name)employees 保存员工、所在部门、直属负责人、岗位、工资和入职日期:
employees(
employee_id, department_id, manager_id,
employee_name, job_title, salary, hire_date
)inventory_movements 保存每次入库、出库、退货或盘点调整:
inventory_movements(
movement_id, product_id, employee_id,
movement_type, quantity, moved_at, note
)库存流水同时引用商品和员工。于是系统不仅知道“订单 101 让白瓷马克杯减少 1 件”,还知道谁在什么时间执行、属于哪类变动,以及为什么调整。quantity 可以约定入库为正、出库为负,也可以全部存正数,再由 movement_type 决定方向;两种设计都能工作,但全课程必须使用同一套约定。我们会采用“带方向的数量”:入库为正,出库为负,movement_type 说明业务原因。

我们当然可以设计一张 all_shop_data,把客户、订单、商品、付款、员工全塞进去。但一笔订单有三件商品和两次付款时,行数会怎样算?如果把商品与付款直接做笛卡尔组合,就可能得到六行,订单总额和客户资料被重复六次。统计时稍不注意,付款金额也会被重复累计。
更麻烦的是修改。员工调部门,需要更新所有历史库存记录中的部门名称;商品改名,需要更新每笔旧订单;客户改城市,需要更新所有订单副本。插入也会受影响:还没产生订单的新商品放不进以订单为中心的大表;删除客户最后一笔订单时,可能顺手把客户资料也删掉。
拆表的目的不是追求表越多越好,而是让一种事实尽量只保存一次,并把不同变化速度的数据分开。客户城市会变,历史订单不会因此改成别人的;商品当前价格会变,历史成交价仍应保留。合理分表后,每次修改的对象和范围更清楚。
“不要重复”不是绝对禁令。订单明细保留成交单价,就是有意保存历史快照;商品表的当前库存和库存流水也可能同时存在。判断一份数据是否该重复,要问它是不是独立的业务事实,以及两份值发生差异时有没有明确解释。
模型搭好之后,我们需要一种语言与数据库沟通。SQL 最值得先理解的,不是关键字必须大写,也不是每句末尾有分号,而是它主要采用声明式思维:你描述想要的结果及其条件,数据库负责选择执行步骤。
假设运营想看杭州客户:
SELECT customer_id, customer_name, city
FROM customers
WHERE city = '杭州'
ORDER BY customer_id;这句话声明了四件事:结果要哪些列,数据来自哪张表,哪些行能留下,结果按什么规则排序。它没有要求数据库“先打开数据文件,从第一行向后找,每发现一个杭州就复制到内存”。数据库可以扫描整张表,也可以使用城市索引;如果统计信息和数据分布变化,它还可以改用另一条执行路径。

SQL 的书写顺序与我们理解它的顺序不完全一样。初学时,可以先按下面的方式读:
先读 FROM customers:这次问题的基础数据来自客户表。
再读 WHERE city = '杭州':只保留城市等于杭州的行。
接着读 SELECT customer_id, customer_name, city:从留下的行中输出这三列。
最后读 ORDER BY customer_id:按客户编号排列最终结果,避免依赖数据库碰巧返回的顺序。
这个阅读顺序以后会扩展到连接、分组和聚合。先找数据来源,再决定行怎样组合和筛选,然后计算分组,最后形成输出。执行器内部可能有更复杂的优化,但逻辑上这样读,比较不容易写乱。
“数据库决定怎么执行”并不代表 SQL 写法无所谓。筛选条件能否使用索引、连接列类型是否一致、是否读取了不需要的列、统计信息是否可靠,都会影响优化器可选的路径。我们负责准确描述结果并提供合适的数据结构,优化器在这些条件下寻找执行计划。
可以把它想成导航。你提供目的地、出发点和限制条件,导航选择路线;如果目的地写错,再聪明的导航也到不了正确地方。如果道路数据不完整,路线也可能不理想。后面学习索引和 EXPLAIN 时,我们会看到数据库选了哪条路。
如果要同时显示订单号和客户姓名,单张 orders 表不够,因为它只有 customer_id。我们需要按共同编号连接客户和订单:
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;使用前面的示例数据,结果是:
o 和 c 是表别名,分别代表订单表和客户表。ON 后面写连接条件:订单的客户编号与客户表的客户编号相等。SQL 不关心客户行与订单行在磁盘上是否挨着,它只根据值相等建立联系。这也是关系模型与按指针追踪固定路径的一处关键差别。
SELECT 读取并返回结果,不会因为筛选掉某些行就把它们从原表删除。下面的查询只显示有效销售状态的商品:
SELECT product_id, product_name, unit_price, stock
FROM products
WHERE status = '在售'
ORDER BY product_id;没有出现在结果里的停用商品仍然保存在表中。真正增加、修改和删除数据,需要 INSERT、UPDATE、DELETE。真正改变表结构,则会使用 CREATE、ALTER、DROP。先把“得到一个结果”和“改变数据库状态”分开,能避免不少危险操作。
这些分类帮助我们建立地图,但不必急着背缩写。学到具体业务时再看语句如何协作,比单独记 DDL、DML、DCL 更牢靠。
SQL 有标准,各数据库产品也有自己的实现。课程选择 MySQL 8.4 系列作为练习环境,默认使用 InnoDB 存储引擎与 utf8mb4 字符集。绝大多数关于表、筛选、连接、聚合和事务的思路可以迁移到 PostgreSQL 等关系型数据库,但数据类型、函数、自动编号、标识符规则和部分语法会有差异。
MySQL Server 是真正保存数据、检查约束并执行 SQL 的服务进程。mysql 是随 MySQL 提供的命令行客户端,负责把你输入的语句发送给服务器并显示结果。图形化客户端也是客户端,只是用窗口展示对象和结果。
所以,“打开客户端”不等于数据库服务已经运行,“安装了服务”也不等于已经连接成功。遇到问题时先判断是哪一层:服务器没启动、连接参数不对、用户权限不足,还是 SQL 本身有错误。
进入 MySQL 客户端后,可以先运行几条不会修改数据的查询:
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:
SELECT
'小满商店' AS shop_name,
ROUND(39.90 + 89.00 * (1 - 0.1124), 2) AS order_amount;这条语句证明客户端、服务器和中文显示已经能协作。MySQL 允许 SELECT 在不写 FROM 的情况下计算表达式。这里只做连通性检查,不代表以后把金额计算都写成常量。
课程示例会把 SQL 关键字写成大写,把表名和列名写成小写蛇形命名,例如 ORDER BY、order_items、registered_at。MySQL 对许多关键字不区分大小写,但统一样式能让结构更容易扫读。
每条完整语句以分号结束。短语句可以写一行,业务查询按子句换行,连接条件再缩进一级。字符串与日期时间使用单引号,数字不加引号。注释使用 -- ,两个连字符后保留一个空格:
SELECT customer_id, customer_name
FROM customers
WHERE city = '杭州' -- 字符串使用单引号
ORDER BY customer_id;表名、列名尽量使用英文标识符,展示值可以使用中文。这样既便于跨工具协作,也避免不同系统对中文标识符的转义和大小写处理差异。英文标识符并不妨碍我们用中文理解业务:order_items 就是订单明细,inventory_movements 就是库存变动流水。
语句写错时,服务器通常会返回错误编号、状态和附近位置。不要只看“报错了”,先从三个方向检查:
例如,在业务表还没创建时直接运行:
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 表示值缺失、未知或不适用;'' 是长度为零的已知字符串;0 是明确的数字。没有推荐人时 referrer_id 可以是 NULL,库存为 0 则表示明确知道没有可用库存。后面学习筛选时会看到,NULL 不能用普通等号判断。
即使连续十次查询都按主键顺序返回,只要没写 ORDER BY,这个顺序就不是查询承诺的一部分。数据量、索引和执行计划一变,结果可能重排。只在展示或业务逻辑确实需要顺序时写排序,并明确用哪些列处理并列情况。
把所有信息塞进一个文本列,也能用字符串函数勉强查;没有外键,也能手工连接;价格全部存成字符,也能临时转换。这些写法的问题会在数据增长、多人修改和需求变化时放大。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;这条查询已经包含本章的主线:customers 和 orders 各保存一种事实,主键与外键提供连接条件,FROM 和 JOIN 确定数据来源,WHERE 声明筛选规则,SELECT 决定输出列,ORDER BY 承诺结果顺序。你不需要现在记住每个关键字的全部细节,只要能顺着业务意思读懂它。
小满商店曾用一张表记录“订单号、客户姓名、客户电话、商品 1、商品 2、商品 3、付款金额”。请回答两个问题:
下面的查询要回答什么问题?如果去掉 ORDER BY,结果内容和结果顺序分别会发生什么变化?
SELECT product_id, product_name, stock
FROM products
WHERE status = '在售'
AND stock < 40
ORDER BY stock, product_id;在进入下一章之前,试着不看前文回答下面五句话:
order_items?ORDER BY?如果你能用自己的话说清楚,就已经抓住了关系型数据库的骨架。下一步不是再加一批术语,而是把这套骨架写成 MySQL 能执行的定义:创建 xiaoman_shop 数据库,为九张表选择数据类型,声明主键、外键与其他约束,再放入第一批可供后续章节查询的数据。