一、现象
sql
-- 注意:id 字段是 varchar 类型
highgo=# create table test(id varchar,name varchar);
CREATE TABLE
highgo=# insert into test values ('1','a'),('2','b'),('3','c'),('4','d'),('5','e'),('6','f'),('7','g'),('8','h'),('9','i'),('10','j'),('100','k'),('1000','l');
INSERT 0 12
highgo=# select * from test;
id | name
------+------
1 | a
2 | b
3 | c
4 | d
5 | e
6 | f
7 | g
8 | h
9 | i
10 | j
100 | k
1000 | l
(12 rows)
想查询 id 大于 '5' 的所有记录,于是执行了下面这条SQL,本以为会得到 6,7,8,9,10,100,1000,结果却只返回了 4 条:
sql
highgo=# select * from test where id > '5';
id | name
----+------
6 | f
7 | g
8 | h
9 | i
(4 rows)
10、100、1000 哪去了? 难道它们不比 5 大吗?更诡异的是,如果去掉引号再查,10、100、1000 又回来了!:
sql
highgo=# select * from test where id > 5;
id | name
------+------
6 | f
7 | g
8 | h
9 | i
10 | j
100 | k
1000 | l
(7 rows)
二、原因:ASCII码字典序 vs 数值序
1. 加了引号 >,是字符串比较
当条件写成 WHERE id > '5' 时,数据库会把它当作字符串(文本)之间的比较 。字符串比较的规则是:从左到右,逐位比较ASCII码值(也就是我们常说的字典序)。
- 字符
'6'的ASCII码是 54 ,'5'是 53 ,所以'6' > '5'成立。 - 但是,
'10'的第一个字符是'1',ASCII码是 49 。49比53('5')小,所以比较器直接判定'10' < '5',根本不会去看后面的'0'。 - 同理,
'100'和'1000'的首字符也都是'1',统统被判为小于'5'。
这就像我们查字典时,所有以 "A" 开头的单词,永远排在所有以 "B" 开头的单词前面 ,不管它后面有多长。甚至,如果你查 WHERE id > '9',返回结果是空(0行),因为 '10' 的首字符 '1' 小于 '9'。
2. 不加引号 > 5,是数值比较
当条件写成 WHERE id > 5 时,数据库发现左边是 VARCHAR,右边是 INTEGER,类型不匹配。于是,数据库默默做了隐式类型转换 ,把 id 字段的内容转换成了数值类型(比如 double precision)再去比较。这时候就是实打实的数值比大小了:6 > 5、10 > 5、100 > 5、1000 > 5 全都成立,所以它们都回来了。通过执行计划可观察到是数值还是字符串比较:
sql
highgo=# explain(analyze,buffers) select * from test where id > '5';
QUERY PLAN
--------------------------------------------------------------------------------------------------
Seq Scan on test (cost=0.00..20.12 rows=270 width=64) (actual time=0.007..0.007 rows=4 loops=1)
Filter: ((id)::text > '5'::text)
Rows Removed by Filter: 8
Buffers: shared hit=1
Planning Time: 0.022 ms
Execution Time: 0.020 ms
(6 rows)
highgo=# explain(analyze,buffers) select * from test where id > 5;
QUERY PLAN
--------------------------------------------------------------------------------------------------
Seq Scan on test (cost=0.00..24.18 rows=270 width=64) (actual time=0.011..0.012 rows=7 loops=1)
Filter: ((id)::double precision > '5'::double precision)
Rows Removed by Filter: 5
Buffers: shared hit=1
Planning Time: 0.043 ms
Execution Time: 0.021 ms
(6 rows)
三、解决方案
为了避免用户因为这种规则查不到想要的数据,我们在设计和写SQL时,有几种解法:
方案一:建表时用对数据类型(最推荐)
如果 id 本身就是一个数字编号,建表时直接使用 INTEGER 或 BIGINT ,不要用 VARCHAR。这是从根源上解决问题。
sql
CREATE TABLE test (
id INT, -- 直接定义为数字类型
name VARCHAR
);
方案二:查询时显式类型转换(适用于字段无法改类型)
如果字段已经定死了是 VARCHAR 且改不了,那就在查询时手动把字符串转成数字:
sql
SELECT * FROM test WHERE id::INT > 5;
-- 或者
SELECT * FROM test WHERE CAST(id AS INTEGER) > 5;
注意:这种写法会走不了索引(除非建函数索引),数据量大的时候要小心性能。
方案三:如果必须按字符串存,且想按数值排序/比较
如果字段必须存字符串(比如存的是 'A001'、'B002' 这种),那就老老实实用字典序,并提前给数字补零(如 '001'、'010'、'100'),保证位数一致。但显然,这不适用于现在这种纯数字场景。