操作符
操作符看起来只是一个符号,例如 +、= 或 ||,但数据库执行它时必须先回答两个问题:这个符号对应哪个操作实现?每个操作数应该是什么类型? 类型转换正是在这个解析阶段参与的。
本文以通用 SQL 为主。操作符重载、:: 写法和候选操作符选择规则在 PostgreSQL 中尤其明显;其他数据库可能使用不同的优先级或只提供少量固定类型组合。
从表达式到结果
SELECT 1 + 2 AS total,
'10' = 10 AS same_value;
数据库大致会按以下步骤处理每个操作符:
- 读取操作符左右两侧的表达式,并确定它们当前的类型。
- 找到名称和参数个数相同的候选操作符。
- 判断哪些候选可以通过隐式转换接收这些参数。
- 根据精确匹配、类型类别和“首选类型”等规则选出唯一实现。
- 执行实现,并确定整个表达式的结果类型。
如果没有候选实现,查询会报“操作符不存在”一类的错误;如果有多个候选都同样合适,可能报“操作符不明确”。
操作数的类型从哪里来
列和参数通常已经有明确类型,字面量则需要特别注意:
SELECT 42, 42.0, '42', NULL;
42通常先按整数类型处理;42.0通常按精确小数或近似数值处理,具体取决于数据库;'42'在某些数据库中先作为未知类型的字符串字面量,等上下文决定它是文本还是数字;NULL没有具体值,也没有足够信息单独决定目标类型。
因此,下面两条语句的可移植性和含义并不相同:
SELECT '10' || '20'; -- 通常是字符串拼接
SELECT CAST('10' AS INT) + 20;
第二条明确告诉数据库要做数值加法。涉及接口参数、金额或连接键时,不要依赖数据库猜测字符串字面量的类型。
一元和二元操作符
一元操作符只有一个操作数,二元操作符有两个操作数:
SELECT -quantity AS negative_quantity,
price * quantity AS amount,
NOT is_deleted AS active;
- 需要数值类型,NOT 需要布尔类型。若 quantity 是字符列,数据库是否会自动把它转成数字取决于方言;即使能够执行,也可能在遇到非法文本时才失败。
-- 更明确,也更容易发现脏数据
SELECT CAST(quantity_text AS INTEGER) * unit_price
FROM order_items;
表达式中的每个操作数都应有稳定的业务类型。把转换写在靠近数据入口的位置,通常比在多个操作符之间反复依赖隐式转换更容易维护。
算术操作符与数值提升
常见算术操作符包括 +、-、*、/ 和 %(取模)。不同数据库会把两个输入提升为某个共同数值类型:
SELECT 5 / 2 AS quotient,
CAST(5 AS DECIMAL(10, 2)) / 2 AS exact_quotient;
有些数据库中两个整数相除得到整数 2,有些会得到带小数的结果。需要小数结果时,显式转换至少一个操作数,并为中间结果预留足够精度。/ 0 的行为通常是错误,不要用隐式转换“碰运气”。
数值提升也可能带来精度损失:
SELECT CAST(total AS DOUBLE PRECISION) + adjustment
FROM settlements;
一旦表达式被提升到近似浮点类型,结果就不再适合作为需要十进制精确的金额。财务计算优先统一到 DECIMAL/NUMERIC,并明确舍入时机。
比较操作符和类型一致性
比较操作符包括 =, <>, <, <=, >, >=。比较前应先统一两边的语义类型:
-- 推荐:参数使用 account_id 的真实类型
WHERE account_id = :account_id
-- 临时兼容旧接口时,明确转换参数
WHERE account_id = CAST(:account_id_text AS BIGINT)
不要把索引列转换成文本来适配一个字符串参数:
-- 可能让 account_id 的普通索引无法直接使用
WHERE CAST(account_id AS VARCHAR(30)) = :account_id_text
数字 2 与文本 '02' 在数值语义下可以相等,在字符串语义下却不一定相等。日期、大小写、前导空格也会受到相同问题影响。比较条件中的转换方向应由业务语义决定,而不是由某个示例数据“刚好能运行”决定。
字符串拼接不是数值加法
许多数据库使用 || 拼接字符串,部分数据库使用 CONCAT 或 +:
SELECT first_name || ' ' || last_name AS full_name
FROM users;
|| 两侧通常需要字符类型。数字拼接前先显式转换;如果任一输入为 NULL,结果是否为 NULL 或被当作空字符串也可能有方言差异:
SELECT CAST(order_id AS VARCHAR(30)) || ':' || status
FROM orders;
需要跨数据库时,优先使用文档明确支持的字符串函数,并单独测试 NULL 行为。
布尔操作符和三值逻辑
AND、OR、NOT 的输入是布尔值,结果遵循 TRUE、FALSE、UNKNOWN(通常由 NULL 表示)的三值逻辑:
| A | B | A AND B | A OR B |
|---|---|---|---|
TRUE | UNKNOWN | UNKNOWN | TRUE |
FALSE | UNKNOWN | FALSE | UNKNOWN |
UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN |
WHERE 只保留条件为 TRUE 的行,FALSE 和 UNKNOWN 都会被过滤。不要把 NULL 当成 FALSE,必要时用 COALESCE 明确默认值:
WHERE COALESCE(is_enabled, FALSE) AND NOT is_deleted
如果数据库没有原生布尔类型,1/0、'Y'/'N' 等表示法不能直接当作通用规则使用,应先映射为明确的逻辑值。
NULL 与操作符
大多数普通操作符只要有一个操作数为 NULL,结果通常就是 NULL:
SELECT 10 + NULL AS unknown_total,
NULL = NULL AS unknown_comparison;
第二列不是 TRUE。判断空值必须使用 IS NULL 或 IS NOT NULL:
WHERE shipped_at IS NULL
某些数据库提供 IS DISTINCT FROM,它把两个 NULL 视为相等,从而得到不含未知结果的比较。该语法不是所有数据库都支持,迁移时应核对方言。
数组、JSON 和其他专用操作符
数组、JSON、范围和全文搜索通常有自己的操作符集合。例如 PostgreSQL 中:
SELECT tags @> ARRAY['sql']::text[]
FROM articles;
这里既有数组包含操作符 @>,又有 PostgreSQL 风格的 :: 转换。ARRAY['sql'] 的元素类型和列的数组类型需要兼容;若数据库无法从上下文推断类型,应显式写出目标类型。
专用操作符的可移植性低于 =、+、CAST 等基础语法。使用前应确认操作数类型、NULL 规则、索引支持和目标数据库版本。
自定义操作符与重载
支持用户定义操作符的数据库允许同一个符号对应多个参数类型。重载提升了表达能力,也增加了歧义风险:
-- 假设数据库同时定义了 text = text 和 uuid = uuid
WHERE id = '550e8400-e29b-41d4-a716-446655440000'
如果 id 是 UUID 列,通常会根据列类型把字面量转换为 UUID;如果两边都是未知字面量或自定义类型较多,就可能无法选出唯一实现。显式 CAST 能让解析结果稳定,也能让审查者看懂意图。
求值顺序不是类型转换顺序
SQL 是声明式语言,不能依赖 AND 或 OR 左侧一定先执行来保护右侧转换:
-- 不能保证只有数字文本才会执行 CAST
WHERE raw_value ~ '^[0-9]+$'
AND CAST(raw_value AS INTEGER) > 0
优化器可能重排条件,或者在计划阶段推断表达式类型。处理脏数据时,应使用数据库提供的安全转换函数,或先把数据清洗到类型正确的列,而不是依靠布尔条件的短路效果。
对性能的影响
类型转换本身未必昂贵,但在每行执行且阻断索引时会明显影响查询:
-- 可能无法使用 created_at 的普通索引
WHERE CAST(created_at AS DATE) = :day
可以改写为范围条件,让列保持原类型:
WHERE created_at >= :day_start
AND created_at < :next_day_start
最终应查看目标数据库的执行计划。若业务确实需要表达式查询,可考虑生成列或表达式索引,但这属于数据库特性,不能默认跨平台可用。
排查操作符错误的方法
遇到“操作符不存在”或“类型不明确”时,可以按这个顺序排查:
- 查看每个列、参数和字面量的实际类型。
- 确认使用的是该数据库支持的操作符,而不是另一种方言的写法。
- 在关键位置加
CAST,让两边表达式拥有同一语义类型。 - 检查
NULL、空字符串、前导零和非法格式等边界输入。 - 重新查看执行计划,确认转换没有落在索引列上。
小结
- 操作符解析同时决定“使用哪个实现”和“如何转换操作数”。
- 整数除法、字符串拼接、布尔三值逻辑和
NULL是最容易被隐式规则影响的场景。 - 重要比较应统一类型,优先转换参数而不是索引列。
- 操作符重载和专用类型能提供强大能力,但要用显式转换消除歧义。
- 不要依赖条件短路来保护可能失败的转换;用安全转换或预先清洗数据。