🪟 第8章 SQL数据查询基础
📄 一句话概括

查询的基本形状:先挑列(SELECT)、再挑行(WHERE)、最后排序(ORDER BY)。 计算列是「查的时候算」,不存盘;改了源数据,下次查结果立刻变。 SELECT INTO 把结果存成新表,比 INSERT INTO ... SELECT 快——但它只带走数据, 不带约束和索引。

📝 本章和前面几章的关系

第 6、7 章讲的是「语言本身」:语句怎么组织、函数怎么算。 从这一节开始讲「查数据」——这是 SQL 用得最多的一块,也是最容易写错的一块。 教材 8.1a 分三小节:SELECT...FROM 子句、列别名与计算列、INTO 子句。 前两节是基础,第三节是个实用技巧——做临时备份、做中间表都用它。 演示里真跑了一批商品数据:挑列、起别名、算金额、再存成一张新表。 SELECT INTO 是本沙箱跑不了的(它是 SQL Server 语法), 用 CREATE TABLE AS SELECT 等价复刻。

一、最基本的形状

所有查询都是这个形状的变体:

SELECT 列清单
FROM 表
[WHERE 条件]
[ORDER BY 排序列]

方括号里的是可选的。 但实际写的时候, ORDER BY 建议都带上——不写它,返回的顺序不保证。 这一点后面 8.1b 会细说。 SELECT * 看着方便,但生产代码里不建议。 两个理由:一是列顺序一变,依赖它的程序就出错; 二是它会把大字段一起取出来——比如存图片的 varbinary 列,白白多传几兆。 要挑列就写清楚列名。 这多打几个字, 换的是「表结构改了这句还能跑」——而且看代码的人一眼知道你要什么。

二、别名与计算列

别名用 AS,AS 可以省略,但建议写上。 少一个字符, 多一分清楚——尤其是别名跟前一个词连在一起的时候。

SELECT gname AS 商品,
       price AS 单价,
       price * stock AS 金额,
       ROUND(price * 0.9, 2) AS 九折价
FROM tb_goods

后两行是计算列:查询的时候现算,不存盘。好处是永远跟源数据一致——改了 price, 下次查金额就变了;代价是每次查都要算一遍,数据量大、算得复杂的时候会拖慢。 如果这个计算很常用、数据量又大,可以考虑建「计算列」把它存起来。那是另一回事—— 建的时候要权衡「存的空间」和「算的时间」。 中文别名不用加引号。 因为它是合法的 Unicode 标识符——6.2 讲命名规则时说过。 只有别名里带空格才要方括号:AS [单 价]。单引号不行,那是字符串不是标识符。

三、SELECT INTO:把结果存成表

SELECT id, gname, price
INTO tb_goods_bak
FROM tb_goods
WHERE price > 100

这是 SQL Server 的写法,本沙箱跑不了。 SQLite 的等价写法是 CREATE TABLE tb_bak AS SELECT ...——思路一样:拿查询结果建一张新表。 新表的结构是照「查询结果」建的,不是照原表。上面只 SELECT 了三列, 新表就只有三列——原表的主键、默认值、索引一个都不会跟过来。 所以它适合「临时备份」和「做中间表」,不适合当正式表。要建正式表, 还是老老实实写 CREATE TABLE——把类型、约束、索引都定清楚。 SELECT INTO 比 INSERT INTO ... SELECT 快, 因为它不记完整日志——它是「建表 + 插入」一次完成,属于批量操作。 数据量大的时候差别明显。

四、三个容易忽略的细节

细节 说明
别名只在客户端 price AS 单价 之后,表里还是 price
计算列不存盘 每次查都重算,改了源数据立刻变
INTO 只带数据 约束、索引、默认值都不跟过来

第一条经常被误解。 有人以为起了别名就改了列名, 结果在 WHERE 里写 WHERE 单价 > 100 报错——因为 WHERE 执行时别名还不存在。 第二条在性能上要留心。 一个查询里算五个计算列, 每个都要扫一遍数据——能用一次算完的,就别拆成五次。 第三条是数据安全上的考虑。 拿 SELECT INTO 备份出来的表, 没有主键、没有非空约束——往里插脏数据它照收。临时用没问题,长期用要补约束。

五、动手 Demo:挑列、起别名、算金额、存成新表

这一段用七行商品数据演四件事:最基本的筛行、列别名与计算列、中文别名要不要加引号、 以及把查询结果存成一张新表(SELECT INTO 的等价写法)。

import sqlite3

c = sqlite3.connect(u":memory:")
c.execute(u"CREATE TABLE tb_goods (id INTEGER PRIMARY KEY, gname TEXT,"
          u" cat TEXT, price REAL, stock INT)")
c.executemany(u"INSERT INTO tb_goods VALUES (?,?,?,?,?)", [
    (1, '笔记本', '电脑', 4999.0, 12), (2, '键盘', '电脑', 199.0, 40),
    (3, '鼠标', '电脑', 89.5, 0),     (4, '显示器', '电脑', 1299.0, 7)])

# 别名 + 计算列
q = u"""SELECT gname AS 商品, price AS 单价,
              price * stock AS 金额,
              ROUND(price * 0.9, 2) AS 九折价
       FROM tb_goods WHERE stock > 0"""
for r in c.execute(q):
    print(r)

# SELECT INTO 的等价写法
c.execute(u"CREATE TABLE tb_bak AS SELECT id, gname, price "
          u"FROM tb_goods WHERE price > 100")
print(u"新表列数:", len(c.execute(u"PRAGMA table_info(tb_bak)").fetchall()))
print(u"新表行数:", c.execute(u"SELECT COUNT(*) FROM tb_bak").fetchone()[0])

实跑输出:

--------------------------------------------------------------------
SELECT...FROM 与 INTO
--------------------------------------------------------------------


  ① 最基本的形状
  --------------------------------------------------------------------
  SELECT 列清单 FROM 表 [WHERE 条件] [ORDER BY 排序列]

  id  gname  price
  1   笔记本    4999.0
  2   键盘     199.0
  4   显示器    1299.0
  这是 SQL 里用得最多的形状:先挑列,再挑行,最后排序。
  SELECT * 看似方便,但生产代码里不建议——列顺序一变,依赖它的程序就出错。
  而且 * 会把大字段一起取出来(比如存图片的 varbinary),白白多传几兆。

  ② 列别名与计算列
  --------------------------------------------------------------------
  商品   单价       库存   金额        九折价
  笔记本  4999.00  12   59988.00  4499.10
  键盘   199.00   40   7960.00   179.10
  显示器  1299.00  7    9093.00   1169.10
  打印纸  25.00    200  5000.00   22.50
  订书机  18.50    60   1110.00   16.65
  文件夹  6.00     150  900.00    5.40
  别名用 AS,AS 可以省略但建议写上——少一个字符,多一分清楚。
  别名只在客户端显示,不改变表结构。所以 price AS 单价 之后,表里还是叫 price。
  计算列是「查询时算出来的」,不存盘。每次查都重算,改了 price 结果立刻变。
  如果这个计算很常用、而且数据量很大,可以考虑建「计算列」存起来——那是另一回事。

  ③ 中文别名要不要加引号
  --------------------------------------------------------------------
| 写法 | 能用吗 |
|---|---|
| AS 单价 | 能 |
| AS [单 价] | 能 |
| AS '单价' | 不能 |

  中文别名不加引号就能用,因为它是合法的 Unicode 标识符(6.2 讲过命名规则)。
  空白别名才需要方括号:[单 价]。单引号不行——那是字符串,不是标识符。

  ④ SELECT INTO:把结果存成一张新表
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | SELECT id, gname, price
  | INTO tb_goods_bak
  | FROM tb_goods
  | WHERE price > 100

  这是 SQL Server 的写法,本沙箱跑不了;SQLite 的等价写法是 CREATE TABLE AS SELECT。
  CREATE TABLE tb_bak AS SELECT ... 之后:
    新表 3 行,源查询 3 行
  新表只有三列——因为 SELECT 里只写了三列。结构是照查询结果建的,不是照原表。
  约束、索引、默认值都不会跟过来。所以它适合「临时备份」和「做中间表」,不适合当正式表。
  SELECT INTO 比 INSERT INTO ... SELECT 快,因为它是「建表 + 插入」一次完成,不记完整日志。

第一段四行里,金额 和 九折价 都是算出来的。 注意 金额 用的是 price * stock, 那两列原本不在查询结果里——计算列可以从任意列算,不用先选出来。 鼠标的库存是 0,被 WHERE stock > 0 筛掉了。 所以只有三行—— 这是「先筛行再挑列」的顺序,虽然写的时候 SELECT 在前面。 第二段是两个数:列数 3、行数 3。 列数是 3,因为 SELECT 里只写了三列; 行数是 3,因为价格大于 100 的有笔记本、键盘、显示器三个——鼠标 89.5 不够。 新表里没有主键、没有索引、也没有非空约束。 这就是「INTO 只带数据」的意思—— 它能当临时表,不能当正式表。

30 秒实验

  1. 把你常查的一张表,SELECT * 改成写全列名,看返回的字节数少了多少。 如果表里有大字段,差别会很明显。
  2. 写一个计算列,比如 price * 0.9,然后 UPDATE 一下源表的 price,再查一次。 计算列的值跟着变了——这就是「不存盘」的意思。
  3. 想一想:为什么 WHERE 里不能用 SELECT 里起的别名? 因为执行顺序是 WHERE 在前、SELECT 在后。 别名还没算出来,WHERE 就已经要用了。 但 ORDER BY 能用别名——它在 SELECT 之后。

常见误区

  1. 「SELECT * 方便,反正后面会挑」。列顺序一变,依赖它的程序就崩;大字段会被整列取出来。 写清楚列名,多打几个字换稳定。
  2. 「起了别名就是改了列名」。别名只在查询结果里生效,表结构一点没变。 而且 WHERE 里用不了它——因为那时候别名还不存在。
  3. 「SELECT INTO 备份出来的表能当正式表用」。它只有数据,没有主键、索引、非空约束。 往里插脏数据它照收——临时用可以,长期用得补约束。
自测三问(点开对答案) 1. `SELECT INTO` 建出来的新表,结构是照什么建的? —— **照「查询结果」建的,不是照原表。** `SELECT` 里写了几列,新表就有几列; **约束、索引、默认值一个都不会跟过来。** 2. 计算列存在磁盘上吗? —— **不存。** 它是查询时现算的——改了源数据,下次查结果立刻变。 **好处是永远一致,代价是每次都算一遍。** 3. 为什么 `WHERE 单价 > 100` 会报错,而 `ORDER BY 单价` 可以? —— **因为执行顺序不同。** `WHERE` 在 `SELECT` 之前执行,那时候别名还不存在; `ORDER BY` 在 `SELECT` 之后,别名已经算出来了。

下一篇 8.1b 讲 WHERE、GROUP BY、HAVING、ORDER BY 四个子句。 重点是它们的执行顺序——那张顺序表能解释「为什么这里能用别名、那里不能」。

相关笔记