词法结构
数据库在理解一条 SQL 语句之前,会先把连续的字符拆成一个个有意义的最小单位,这些单位称为 词法单元(token)。关键字、表名、数字、字符串、运算符和标点都属于词法单元。
例如:
SELECT name, price * 0.8 AS sale_price
FROM products
WHERE category = 'book';
可以粗略拆成:
SELECT | name | , | price | * | 0.8 | AS | sale_price
FROM | products
WHERE | category | = | 'book' | ;
理解词法结构的价值在于:看懂数据库如何划分一条语句,避免名称、引号和注释引起的语法错误,并更容易区分 SQL 的通用规则与不同数据库的方言差异。
从字符到 SQL 含义
数据库通常按以下过程处理 SQL:
- 词法分析:把字符流拆成关键字、标识符、字面量、运算符等词法单元。
- 语法分析:检查这些词法单元的排列是否符合 SQL 语法。
- 语义分析:确认表和列是否存在、类型是否兼容、当前用户是否有权限等。
- 优化与执行:选择执行计划并访问数据。
因此,不同错误属于不同层次:
SELECT * FORM users;
FORM 无法出现在这个语法位置,属于语法问题;而下面的语句结构正确,但如果 missing_table 不存在,就会在语义分析阶段失败:
SELECT * FROM missing_table;
空白字符
空格、制表符和换行通常只负责分隔词法单元。只要没有把一个词拆开,多数 SQL 可以自由换行:
SELECT id, name
FROM users
WHERE status = 'active';
它与下面的写法通常等价:
SELECT id, name FROM users WHERE status = 'active';
但两个相邻单元可能连成另一个单元时,空白不能省略:
SELECT username FROM users; -- SELECT 与 username 之间必须分隔
推荐用换行和缩进表达结构:每个主要子句单独一行,多列或复杂表达式按层级缩进。格式不会改变查询含义,却会显著降低阅读成本。
注释
注释用于解释 SQL,不参与普通语句的计算。
单行注释
双连字符 -- 开始单行注释,注释一直延续到行尾:
SELECT id, name
FROM users
WHERE status = 'active'; -- 只查询启用的用户
为了兼容不同数据库及客户端,建议在 -- 后保留一个空格。
块注释
/* 和 */ 包围块注释,可以跨越多行:
/*
统计已经支付的订单金额。
退款订单由另一条查询处理。
*/
SELECT SUM(amount)
FROM orders
WHERE status = 'paid';
不要依赖块注释嵌套,因为各数据库的支持情况不同。部分数据库还会把特定格式的注释解释为优化器提示,因此从其他系统复制 SQL 时,需要确认注释是否真的只起说明作用。
关键字
关键字是 SQL 预先定义的单词,例如 SELECT、FROM、WHERE、INSERT 和 NULL。它们描述语句结构或具有特殊含义。
SQL 关键字通常不区分大小写,下面两条语句表达相同意图:
select id from users;
SELECT id FROM users;
常见风格是关键字大写、对象名小写,这只是为了便于阅读,不会让查询执行得更快。
有些关键字是保留字,不能直接用作表名或列名;保留字集合又可能随数据库和版本变化。应优先使用有业务含义且不冲突的名称,而不是依赖引号创建 order、group 等名称。
标识符
标识符用于命名数据库对象,例如数据库、模式、表、列、视图、索引和别名:
SELECT u.display_name AS customer_name
FROM users AS u;
这里的 u、display_name、customer_name 和 users 都是标识符。
普通标识符
普通标识符不加引号。为获得较好的可移植性,建议遵循这些规则:
- 使用英文字母开头;
- 后续只使用英文字母、数字和下划线;
- 不使用空格、连字符或其他特殊字符;
- 避免 SQL 保留字;
- 统一采用一种命名方式,例如
snake_case。
CREATE TABLE order_items (
item_id INTEGER,
product_name VARCHAR(100)
);
普通标识符的大小写处理因数据库而异。有些数据库会折叠为大写或小写,有些会保留原样但比较时忽略大小写。最稳妥的做法是统一使用小写名称,并避免只靠大小写区分两个对象。
分隔标识符
标准 SQL 使用双引号包围需要精确保留的标识符:
SELECT "Order Total"
FROM "Daily Report";
双引号内可以包含空格或关键字,大小写通常也会被精确保留。但这会迫使后续查询一直正确地加引号,因此不应把它作为日常命名方式。
常见数据库存在方言差异:
| 数据库 | 常见标识符引号 | 说明 |
|---|---|---|
| PostgreSQL | "name" | 双引号符合标准 SQL |
| MySQL | `name` | 默认常用反引号;特定 SQL 模式可用双引号 |
| SQL Server | [name] | 常用方括号;相应设置下也支持双引号 |
| SQLite | "name" | 推荐使用标准 SQL 的双引号 |
迁移 SQL 时,要先检查这些引号是否适用于目标数据库。
字面量
字面量是在 SQL 文本中直接写出的值,例如字符串 'Alice'、数字 42 和关键字 NULL。
字符串字面量
字符串通常使用单引号:
SELECT *
FROM users
WHERE city = 'Shanghai';
字符串中需要表示单引号时,标准写法是连续写两个单引号:
SELECT 'It''s SQL' AS message;
结果字符串是 It's SQL。不要用双引号表示普通字符串,因为标准 SQL 把双引号用于标识符。部分数据库模式可能接受双引号字符串,但这种写法不利于移植。
反斜杠转义、原始字符串、美元引用和 Unicode 字符串的规则在各数据库中差异较大。遇到换行、反斜杠或特殊编码时,应查询所用数据库对应版本的文档。
更重要的是,不要通过拼接用户输入来构造字符串:
错误思路:"SELECT * FROM users WHERE name = '" + input + "'"
应使用数据库驱动提供的参数化查询。参数的占位符可能是 ?、:name、$1 或 @name,具体由数据库和驱动决定。参数不仅能正确处理引号和特殊字符,也是防止 SQL 注入的关键措施。
数值字面量
整数、小数和科学计数法通常可以直接书写:
SELECT 42, -7, 3.14, 6.02E23;
负号通常是一元运算符,而不是数值本身的一部分。小数点始终写作 .,不要使用地区格式中的逗号,也不要加入千位分隔符:
SELECT 1234.56; -- 推荐
精确金额应使用数据库提供的精确数值类型,例如 DECIMAL 或 NUMERIC;词法上能写出一个小数,并不代表其最终存储类型一定精确。
布尔值与空值
标准 SQL 使用 TRUE 和 FALSE 表示布尔值,使用 NULL 表示缺失或未知:
SELECT * FROM tasks WHERE completed = TRUE;
SELECT * FROM users WHERE deleted_at IS NULL;
不同数据库对布尔类型的支持并不完全一致,某些系统使用 1 和 0 代替。NULL 也不是字符串 'NULL',判断空值必须使用 IS NULL 或 IS NOT NULL,不能使用 = NULL。
日期和时间
标准 SQL 支持带类型前缀的日期和时间字面量:
SELECT DATE '2026-09-15';
SELECT TIME '14:30:00';
SELECT TIMESTAMP '2026-09-15 14:30:00';
但数据库对这些写法、时区和自动类型转换的支持不同。应用程序写入日期时间时,优先使用参数并绑定明确的目标类型,避免依赖会受地区格式影响的字符串解析。
运算符与特殊符号
运算符对值进行计算或比较:
| 类别 | 常见符号或关键字 | 示例 |
|---|---|---|
| 算术 | +、-、*、/、% | price * quantity |
| 比较 | =、<>、<、<=、>、>= | age >= 18 |
| 逻辑 | NOT、AND、OR | active AND verified |
| 空值判断 | IS NULL、IS NOT NULL | email IS NOT NULL |
| 模式匹配 | LIKE | name LIKE 'Li%' |
!=、字符串连接、正则匹配、JSON 运算符和类型转换运算符等写法具有更多方言差异。需要跨数据库运行时,应优先选择标准形式,例如用 <> 表示“不等于”。
标点符号用于组织结构:
- 逗号
,分隔列、表达式或参数; - 圆括号
()控制运算顺序、包围函数参数或子查询; - 点号
.限定对象名称,例如users.id; - 分号
;表示一条语句结束。
字符相同并不意味着作用固定。例如 * 在 SELECT * 中表示选择全部列,在 price * quantity 中则表示乘法;数据库会根据它所在的语法位置判断含义。
分号与语句边界
分号是标准的 SQL 语句结束符:
SELECT COUNT(*) FROM users;
UPDATE users SET active = FALSE WHERE id = 10;
某些交互式客户端在一次只执行一条语句时允许省略分号,但脚本中应保留它,以便可靠地区分相邻语句。
需要注意,客户端的批处理命令不一定属于 SQL。例如 SQL Server 工具中的 GO、MySQL 客户端中的 DELIMITER 常由客户端预处理,数据库 SQL 语法本身并不把它们当作通用语句结束符。
最长匹配与容易混淆的边界
词法分析器通常会尽可能读取能够组成一个合法词法单元的最长字符序列。因此,相邻字符是否留空格有时会改变含义或直接导致错误。
SELECT 10 - -2; -- 减去负二,空格让意图更清楚
SELECT a >= 10; -- >= 是一个比较运算符
还要避免把标识符、关键字和数字粘在一起:
SELECT order_id FROM orders; -- order_id 是一个完整标识符
当运算符相邻、注释紧邻表达式或方言定义了自有运算符时,显式加入空格通常更清晰。
名称、值与参数不要混淆
这是词法结构中最实用的区分:
| 想表达的内容 | 示例 | 常见写法 |
|---|---|---|
| SQL 结构 | 查询、过滤 | SELECT、WHERE |
| 对象名称 | 表名、列名 | users、display_name |
| 数据值 | 姓名、金额 | 'Alice'、19.90 |
| 动态数据值 | 用户提交的姓名 | 参数占位符,例如 ? 或 $1 |
参数只能代替数据值,通常不能代替表名、列名或排序方向。例如下面的意图一般无法通过普通值参数实现:
SELECT * FROM ?;
动态对象名必须由应用程序在可信的白名单中选择,再按照所用数据库的规则安全引用。不要把任意用户输入直接拼接到 SQL 结构中。
完整示例
下面的语句包含本节的大多数词法元素:
-- 查询指定日期之后的已支付订单
SELECT
o.order_id,
o.customer_name,
o.amount * 0.9 AS discounted_amount
FROM orders AS o
WHERE o.status = 'paid'
AND o.created_at >= DATE '2026-01-01'
AND o.deleted_at IS NULL
ORDER BY o.created_at DESC;
可以这样辨认:
SELECT、FROM、WHERE、AND、ORDER BY是关键字;orders、o、order_id等是标识符;0.9、'paid'和DATE '2026-01-01'是字面量;*、=、>=是运算符;.、,和;是用于组织结构的特殊符号;--后面的内容是单行注释;- 空格、换行和缩进负责分隔内容并提高可读性。
常见错误
用双引号表示字符串
-- 不推荐:在标准 SQL 中会被当作标识符
SELECT * FROM users WHERE name = "Alice";
-- 正确
SELECT * FROM users WHERE name = 'Alice';
用单引号包围列名
-- 返回固定字符串,而不是 name 列的值
SELECT 'name' FROM users;
-- 读取 name 列
SELECT name FROM users;
忘记转义字符串中的单引号
-- 正确结果:O'Reilly
SELECT 'O''Reilly';
把 NULL 当作普通值比较
-- 错误思路
SELECT * FROM users WHERE email = NULL;
-- 正确
SELECT * FROM users WHERE email IS NULL;
依赖特定数据库的写法
反引号、方括号、反斜杠转义、布尔值、日期字面量和某些运算符都可能是方言特性。编写可移植 SQL 时,应明确目标数据库,并对非标准写法做隔离或说明。
学习检查清单
读完本节后,应能够:
- 区分词法分析、语法分析和语义分析;
- 识别关键字、标识符、字面量、运算符、分隔符和注释;
- 正确使用单引号表示字符串、双引号表示标准分隔标识符;
- 理解普通标识符的大小写与保留字风险;
- 使用
IS NULL判断空值; - 知道分号通常用于结束 SQL 语句,而
GO等可能只是客户端命令; - 使用参数化查询传递动态值,并认识到参数通常不能替代表名和列名;
- 在使用引号、日期、布尔值和特殊运算符前确认目标数据库的方言规则。
词法结构解决的是“这些字符分别是什么”。掌握这一层后,再学习查询、表达式和数据定义语法时,就能更快判断问题究竟来自拼写与引号,还是来自语句结构和数据库对象本身。