SQL数据库学习:从基础查询到多表关联的实用入门

技术教程3小时前发布 jdkjadmin
0 00
SQL数据库学习:从基础查询到多表关联的实用入门

SQL数据库学习:从基础查询到多表关联的实用入门

SQL 是操作关系型数据库的常用语言。学会写查询,不只是记住几条语句,还要理解数据如何组织、结果集如何形成,以及查询在真实项目中的边界。本文使用 SQLite 作为示例数据库,逐步介绍基础查询、条件过滤、分组统计和多表关联,并说明参数绑定、空值、分页与索引等容易出错的地方。

示例代码可以在 SQLite 命令行、图形化数据库客户端或 Python 的 sqlite3 模块中运行。SQLite 适合本地工具、原型、小型应用和学习;生产系统也常使用 MySQL、PostgreSQL、SQL Server 等数据库。它们共享许多 SQL 基础语法,但数据类型、函数、分页方式、并发行为和管理能力并不完全相同。迁移到其他数据库时,应以目标数据库文档为准,而不是假定所有 SQL 都可直接通用。

一、先建立一组可练习的数据

关系型数据库通常把数据放在表中。每张表由行和列组成:行代表一条记录,列代表记录的属性。下面创建顾客、商品、订单和订单明细四张表。订单明细单独存放,是因为一张订单可能包含多个商品,一个商品也可能出现在多张订单中。

PRAGMA foreign_keys = ON;

DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS customers;

CREATE TABLE customers (
    customer_id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    city TEXT,
    created_at TEXT NOT NULL
);

CREATE TABLE products (
    product_id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    category TEXT NOT NULL,
    price REAL NOT NULL CHECK (price >= 0)
);

CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    order_date TEXT NOT NULL,
    status TEXT NOT NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

CREATE TABLE order_items (
    order_id INTEGER NOT NULL,
    product_id INTEGER NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    unit_price REAL NOT NULL CHECK (unit_price >= 0),
    PRIMARY KEY (order_id, product_id),
    FOREIGN KEY (order_id) REFERENCES orders(order_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);

INSERT INTO customers (customer_id, name, city, created_at) VALUES
    (1, '林晓', '上海', '2024-01-10'),
    (2, '周宁', '杭州', '2024-02-03'),
    (3, '陈可', NULL, '2024-03-18');

INSERT INTO products (product_id, name, category, price) VALUES
    (101, '键盘', '外设', 299.00),
    (102, '鼠标', '外设', 129.00),
    (103, '显示器', '显示设备', 1499.00),
    (104, '支架', '配件', 89.00);

INSERT INTO orders (order_id, customer_id, order_date, status) VALUES
    (1001, 1, '2024-04-01', 'paid'),
    (1002, 1, '2024-04-08', 'shipped'),
    (1003, 2, '2024-04-10', 'cancelled');

INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
    (1001, 101, 1, 299.00),
    (1001, 102, 2, 129.00),
    (1002, 103, 1, 1499.00),
    (1003, 104, 1, 89.00);

示例用 INTEGER PRIMARY KEY 作为整数主键。在 SQLite 中,这种写法具有特殊的行标识行为;其他数据库的自增主键定义可能不同。NOT NULL 表示必须提供值,CHECK 用于限制列值,外键则描述表之间的引用关系。SQLite 默认并不总是启用外键检查,因此示例显式执行了 PRAGMA foreign_keys = ON;使用其他数据库时,外键的启用和配置方式应查看对应文档。

二、用 SELECT 读取需要的数据

最基本的查询由 SELECT 和 FROM 组成。正式业务代码一般应明确列名,而不是长期依赖星号。

SELECT customer_id, name, city
FROM customers;

SELECT * 会返回当前表中的所有列,适合快速查看数据,但在应用代码中有几个隐患:表新增列后返回结构可能改变;无用的大字段会增加传输和处理成本;关联多张表时,同名列也容易造成歧义。选择实际需要的列,通常更清晰,也更利于维护。

可以用 AS 给列起一个结果别名。别名只影响查询结果中的显示名称,不会修改表结构。

SELECT
    product_id,
    name AS product_name,
    price AS unit_price
FROM products;

查询结果本身不保证顺序。即便多次运行看起来顺序相同,只要 SQL 没有明确排序条件,就不应依赖这个顺序。

三、用 WHERE 筛选行

WHERE 用于筛选符合条件的行。常用比较运算符包括 =、<>、>、>=、< 和 <=。字符串值需要用单引号括起来。

SELECT product_id, name, price
FROM products
WHERE price >= 100
  AND category = '外设';

AND 表示条件同时成立,OR 表示至少一个条件成立。混用时,建议用括号明确意图,避免读者误解运算优先级。

SELECT order_id, customer_id, status
FROM orders
WHERE (status = 'paid' OR status = 'shipped')
  AND order_date >= '2024-04-01';

IN 可以判断某个值是否属于一组候选值,BETWEEN 用于闭区间判断,也就是包含两端值。

SELECT order_id, status
FROM orders
WHERE status IN ('paid', 'shipped');

SELECT product_id, name, price
FROM products
WHERE price BETWEEN 100 AND 500;

LIKE 用于简单的模式匹配。在 SQLite 中,% 表示任意长度字符,_ 表示一个字符;大小写匹配规则可能受数据库和排序规则影响,不应把它当成统一的全文搜索方案。

SELECT customer_id, name
FROM customers
WHERE name LIKE '林%';

四、NULL 不是普通的空值

NULL 表示缺失、未知或未提供的数据。它不是空字符串,也不等于数字 0。不能用 city = NULL 判断空值,而应使用 IS NULL 或 IS NOT NULL。

SELECT customer_id, name
FROM customers
WHERE city IS NULL;

SELECT customer_id, name
FROM customers
WHERE city IS NOT NULL;

涉及 NULL 的比较通常会得到“未知”,而不是简单的真或假。这会影响 WHERE 条件:只有结果为真的行才会保留。例如 city <> '上海' 不会选出城市为 NULL 的顾客。若业务要求把缺失值也纳入,应明确补充条件,例如:

SELECT customer_id, name
FROM customers
WHERE city <> '上海'
   OR city IS NULL;

可以用 COALESCE 将空值替换成备用值,但替换结果只影响表达式,不会修改表里的原始数据。

SELECT name, COALESCE(city, '未填写') AS city_label
FROM customers;

五、排序、去重与分页

ORDER BY 对结果排序。默认是升序,可以使用 DESC 指定降序。分页查询时,排序尤其重要:没有稳定排序,翻页过程中可能出现重复记录或遗漏记录。

SELECT product_id, name, price
FROM products
ORDER BY price DESC, product_id ASC;

第二个排序字段 product_id 用来打破价格相同的并列关系。可靠的分页排序最好包含一个能让顺序稳定的唯一键,或其他足以确定顺序的字段组合。

DISTINCT 会去除选定列组合中重复的结果行。它不是“随便取一条重复记录”的指令;如果选了多列,只有这些列的组合完全相同才会被视为重复。

SELECT DISTINCT category
FROM products
ORDER BY category;

SQLite 使用 LIMIT 限制结果数量,也可配合 OFFSET 跳过前面的行。

SELECT product_id, name, price
FROM products
ORDER BY product_id
LIMIT 2 OFFSET 1;

大页码分页时,数据库仍可能需要扫描或跳过大量记录,具体成本取决于查询、索引和执行计划。数据量较大时,可以考虑基于上一页最后一条记录的位置继续查询,即游标式分页。其实现取决于排序规则和字段组合,不能简单把所有场景都替换成固定模板。

传给 LIMIT 或 OFFSET 的值应是非负整数。SQLite 支持在这些位置使用参数占位符;其他数据库对语法及参数类型的支持可能不同。若用户输入页码,应在应用层校验范围并转换成整数,不能把未经校验的文本拼接进 SQL。

六、聚合函数与分组统计

聚合函数可以对多行数据进行计算。常见函数有 COUNT、SUM、AVG、MIN 和 MAX。

SELECT
    COUNT(*) AS product_count,
    AVG(price) AS average_price,
    MIN(price) AS lowest_price,
    MAX(price) AS highest_price
FROM products;

COUNT(*) 统计符合条件的行数;COUNT(column) 只统计该列非 NULL 的行。因此,如果某列允许为空,两者结果可能不同。SUM、AVG 等函数通常会忽略空值;具体边界应按目标数据库文档确认。

使用 GROUP BY 可以按一个或多个字段分组,再对每组计算统计值。

SELECT category, COUNT(*) AS product_count, AVG(price) AS average_price
FROM products
GROUP BY category
ORDER BY product_count DESC;

WHERE 在分组前筛选原始行;HAVING 在分组后筛选聚合结果。下面的查询先限定商品类别,再只保留商品数量至少为两件的类别。

SELECT category, COUNT(*) AS product_count
FROM products
WHERE price > 0
GROUP BY category
HAVING COUNT(*) >= 2;

一个常见错误是在含有聚合的查询中,随意选择未分组、也未聚合的列。不同数据库对这类写法的接受程度和返回含义可能不同。稳妥做法是:结果中的普通列放进 GROUP BY,或者明确使用合适的聚合函数。也要注意,给聚合列起的别名是否能在 HAVING 中使用,可能因数据库而异;直接写聚合表达式更可移植。

七、理解 INNER JOIN 与 LEFT JOIN

多表关联的关键是确定两张表通过什么字段匹配。订单表的 customer_id 引用顾客表,订单明细的 product_id 引用商品表。JOIN 条件写在 ON 后面。

INNER JOIN 只返回两边都匹配的行。下面查询每张订单及其顾客姓名:

SELECT
    o.order_id,
    o.order_date,
    o.status,
    c.name AS customer_name
FROM orders AS o
INNER JOIN customers AS c
    ON o.customer_id = c.customer_id
ORDER BY o.order_date, o.order_id;

表别名 o 和 c 可以缩短字段引用。多表查询中,最好始终写出字段所属的表,例如 o.status,避免同名字段造成歧义。

LEFT JOIN 保留左表中的所有行;如果右表没有匹配项,右表字段会以 NULL 返回。它适合查找“有订单和没有订单的顾客”等场景。

SELECT
    c.customer_id,
    c.name,
    o.order_id
FROM customers AS c
LEFT JOIN orders AS o
    ON c.customer_id = o.customer_id
ORDER BY c.customer_id, o.order_id;

要找出没有订单的顾客,可以检查右表中一个不允许为空的主键是否为 NULL:

SELECT c.customer_id, c.name
FROM customers AS c
LEFT JOIN orders AS o
    ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;

这里有一个容易忽略的边界:对 LEFT JOIN 右表的筛选条件放在 WHERE 中,可能会把未匹配行过滤掉,从而让结果表现得像内连接。例如,如果把 WHERE o.status = 'paid' 加到上一个顾客查询中,没有订单的顾客会因 NULL 比较而被排除。若目标是“保留所有顾客,只关联已付款订单”,应把状态条件放进 ON:

SELECT c.customer_id, c.name, o.order_id
FROM customers AS c
LEFT JOIN orders AS o
    ON c.customer_id = o.customer_id
   AND o.status = 'paid';

关联多张表时,行数可能因为一对多关系而增加。下面把订单、明细和商品连接起来,列出订单中的商品:

SELECT
    o.order_id,
    p.name AS product_name,
    oi.quantity,
    oi.unit_price,
    oi.quantity * oi.unit_price AS line_total
FROM orders AS o
JOIN order_items AS oi
    ON o.order_id = oi.order_id
JOIN products AS p
    ON oi.product_id = p.product_id
ORDER BY o.order_id, p.product_id;

在订单与订单明细关联后,一张包含多个商品的订单会出现多行,这是关系本身的结果,不是重复数据。如果把订单总价直接写成 SUM(o.some_amount),而订单金额在关联后重复出现,就可能被重复累计。汇总前要先确认当前结果粒度:是一行代表一张订单、一件明细,还是一个顾客。需要统计订单金额时,可从明细表按订单分组计算,再与订单表关联,或确保聚合基于正确的明细列。

八、用参数化查询处理外部输入

应用程序通常需要按用户输入查询数据。不要把输入直接拼接进 SQL 字符串。应使用数据库驱动提供的参数绑定接口:SQL 结构与参数值分开传递,驱动负责按规则处理参数,可避免把参数值误当成 SQL 代码执行。这是防止 SQL 注入的重要措施,但不能替代权限控制、输入校验、错误处理和安全的数据库配置。

以下是可运行的 Python 示例,使用标准库 sqlite3 查询商品价格下限:

import sqlite3

connection = sqlite3.connect("shop.db")
connection.row_factory = sqlite3.Row

minimum_price = 100

rows = connection.execute(
    """
    SELECT product_id, name, price
    FROM products
    WHERE price >= ?
    ORDER BY price DESC, product_id
    """,
    (minimum_price,)
).fetchall()

for row in rows:
    print(row["product_id"], row["name"], row["price"])

connection.close()

这里的 ? 是 SQLite 的位置参数占位符,参数以序列传入。单个参数也要写成只有一个元素的元组,因此末尾的逗号不能省略。Python 的 SQLite 驱动还支持命名参数等形式;其他语言和数据库驱动可能使用不同占位符,例如 %s、:name 或编号占位符,应按实际驱动文档使用,不能将一种写法机械移植到所有环境。

参数通常用于传递数据值,不用于动态替换表名、列名、关键字或排序方向。下面这种需求不能依靠把列名当成普通值参数来解决:

SELECT name, price
FROM products
ORDER BY ?;

在支持这种语法的数据库里,占位符通常表示一个常量值,而不是列名,因此它不能实现预期的动态排序。若要允许用户选择排序字段,应在应用代码中将输入映射到固定的白名单标识符,再生成 SQL;排序方向也应限制为允许的选项。不要直接把用户提供的列名或 SQL 片段拼接进去。

参数绑定也不是所有输入校验的替代品。例如,搜索框仍可能需要长度限制和业务规则校验;分页数量仍应校验为合理的正整数;用户能访问哪些记录,则必须在查询条件和权限设计中明确控制。

九、常见错误与排查顺序

  • 忘记写关联条件。 多表查询若没有正确的 ON 条件,可能形成大量行的组合,结果数量异常增大。逐一检查关联字段是否正确、数据类型是否匹配,并确认关联关系的方向。
  • 把 NULL 当成普通值。 使用 = NULL 通常得不到预期结果。判断缺失值要用 IS NULL;还要检查聚合和 LEFT JOIN 是否产生了空值。
  • 分页却没有排序。 数据库不承诺未排序结果的顺序。分页查询需要明确的、尽量稳定的排序条件。
  • 在多表查询中漏写表名。 同名列会造成歧义,最好为表取简短别名,并在字段前加上表别名。
  • 混淆 WHERE 与 HAVING。 普通行条件放在 WHERE;聚合分组结果的条件放在 HAVING。
  • 误把 DISTINCT 当成修复工具。 它可能掩盖关联条件错误或粒度理解错误。先查明行数为何增加,再决定是否需要去重。
  • 用字符串拼接用户输入。 参数化查询用于绑定数据值。动态标识符必须经过白名单映射,不能直接信任外部输入。
  • 假定不同数据库语法完全相同。 日期处理、分页、自动生成主键、字符串函数和参数占位符都可能有差异。跨库开发时应测试实际目标数据库。

十、索引入门:让常见筛选更有效

索引可以帮助数据库更快定位数据,但会占用存储空间,也会增加插入、更新和删除时维护索引的成本。是否使用索引、具体如何执行查询,取决于数据规模、查询条件、索引设计和数据库优化器;不能因为某列出现在 SQL 中,就认定加索引一定更快。

外键字段、经常用于筛选或关联的列,是评估索引时可以优先考虑的对象。示例中可以为订单的顾客编号和日期建立索引:

CREATE INDEX idx_orders_customer_id
ON orders(customer_id);

CREATE INDEX idx_orders_date
ON orders(order_date);

如果常见查询同时按顾客编号筛选并按日期排序,也可以评估组合索引:

CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);

组合索引的列顺序会影响可用场景。以 (customer_id, order_date) 为例,它通常更适合先按 customer_id 查找,再进一步使用日期条件的查询;不应假定它与分别建立两个单列索引效果相同。索引选择应结合真实查询测试。

SQLite 可以用 EXPLAIN QUERY PLAN 查看查询计划,辅助了解是否使用了索引。它显示的是计划信息,不是性能保证;要评估实际速度,还需要使用具有代表性的数据和工作负载进行测试。

EXPLAIN QUERY PLAN
SELECT order_id, order_date
FROM orders
WHERE customer_id = 1
ORDER BY order_date;

十一、把查询放进事务中

当多条写操作必须作为一个整体成功或失败时,应使用事务。例如创建一张订单和对应的多条明细,如果中途失败,应回滚已经完成的写入,避免只保存部分数据。事务的具体隔离级别、锁行为和并发能力因数据库而异。

BEGIN;

INSERT INTO orders (order_id, customer_id, order_date, status)
VALUES (1004, 2, '2024-04-15', 'paid');

INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (1004, 101, 1, 299.00);

COMMIT;

如果执行过程中发生错误,应由客户端回滚事务,或让事务管理机制处理,而不是继续提交不完整的数据。Python 的 sqlite3 模块通常也通过连接对象管理事务;具体提交和回滚方式可根据所用驱动的行为选择。

十二、建议的练习路线

  1. 先从单表查询开始:练习选择列、比较条件、IN、LIKE、排序和限制行数。
  2. 专门练习空值:分别比较 IS NULL、COALESCE、COUNT(*) 与 COUNT(column)。
  3. 练习统计:按商品类别统计数量和均价,再加入 HAVING 条件。
  4. 练习关联:分别写出订单与顾客的内连接、顾客与订单的左连接,以及查找无订单顾客的查询。
  5. 先确认查询的行代表什么,再写聚合。订单关联明细后,一行通常代表一条明细,不再是一张订单。
  6. 将示例放进应用代码,用参数绑定传递输入;另外测试非法页码、空值和不存在的关联记录。

SQL 入门的核心不是把所有关键字背下来,而是准确描述想要的数据:从哪些表取数据、表之间如何匹配、哪些行要保留、结果按什么粒度汇总,以及怎样稳定排序。把这些问题先说清楚,再写查询并检查结果行数,通常比盲目调整语句更有效。掌握单表查询、NULL 语义、聚合和连接之后,就已经具备处理许多日常数据读取任务的基础;性能优化和数据库差异则应结合实际目标数据库继续学习与验证。

© 版权声明

相关文章