SQLite NOCASE 的边界:英文能忽略大小写,带重音字母为何仍不同
一组英文测试通过,还不能说明所有名称都适用
给名称列加上 COLLATE NOCASE 后,Alice 与 ALICE 能匹配,容易让人以为数据库已经实现通用的不区分大小写。继续放入带重音字母的名称,却可能发现大写和小写各自成了一条记录。SQLite 内置 NOCASE 只折叠二十六个 ASCII 大写字母,不会执行完整 Unicode 大小写折叠。
这个限制也影响唯一约束。同一张表可以拒绝只差 ASCII 大小写的两个值,却接受视觉相近的其他名称。对于限定为英文代码的字段,这可能完全符合要求;对于多语言展示名称,则必须先定义什么才算同一个名称,不能只凭排序规则的名字判断。
同时测试查询、唯一性与实际保存内容
下面使用 Python 的内存 SQLite 数据库,本文在默认内置排序规则下验证。首先用参数比较 ASCII 字母、带重音字母和德语词形,再建立一个使用 NOCASE 唯一约束的名称表。示例不加载扩展,也不覆盖内置排序规则,因此结果只说明 SQLite 默认行为。
代码保留第一次插入的 Alice,尝试插入 ALICE 时应触发完整性异常;两个带重音的不同大小写名称则都保留下来。最后分别做默认比较和显式 BINARY 比较,确认比较方式可以改变匹配结果,却不会顺便改写已经存储的文本。
AI概念配图,非真实界面
import sqlite3
con = sqlite3.connect(':memory:', isolation_level=None)
def same(left, right):
return con.execute('SELECT ? = ? COLLATE NOCASE', (left, right)).fetchone()[0]
assert same('Alice', 'ALICE') == 1
assert same('Élodie', 'élodie') == 0
assert same('Straße', 'STRASSE') == 0
assert same('Alice', 'Alice ') == 0
print('ASCII / accented / expanded:', same('Alice', 'ALICE'),
same('Élodie', 'élodie'), same('Straße', 'STRASSE'))
con.execute('CREATE TABLE names(name TEXT COLLATE NOCASE NOT NULL UNIQUE)')
con.execute('INSERT INTO names VALUES (?)', ('Alice',))
try:
con.execute('INSERT INTO names VALUES (?)', ('ALICE',))
except sqlite3.IntegrityError:
pass
else:
raise AssertionError('ASCII duplicate accepted')
con.executemany('INSERT INTO names VALUES (?)', [('Élodie',), ('élodie',)])
assert con.execute('SELECT count(*) FROM names').fetchone() == (3,)
assert con.execute('SELECT name FROM names WHERE name = ?', ('alice',)).fetchall() == [('Alice',)]
assert con.execute('SELECT name FROM names WHERE name COLLATE BINARY = ?', ('alice',)).fetchall() == []
assert con.execute('SELECT name FROM names WHERE name = ?', ('élodie',)).fetchall() == [('élodie',)]
print('stored rows:', con.execute('SELECT name FROM names ORDER BY rowid').fetchall())
print('original spelling and explicit BINARY checks passed')
con.close()排序规则决定怎么比较,不负责改写字符串
三次独立比较的结果依次为真、假、假。表中通过 alice 查询可以拿到原始的 Alice,而显式使用 BINARY 查询同一个小写输入时找不到记录。这说明不区分大小写是比较时的行为;如果希望展示名统一为某种写法,需要另行设计保存与展示策略。
唯一约束使用列的排序规则,所以英文大小写不同的第二次插入被拒绝。程序应捕获并解释这个冲突,而不是把原始名称强制覆盖成新输入。是否允许改名、大小写调整是否算更新,以及哪个字段具有身份意义,都属于应用自己的约定。
先界定字符范围,再选择比较协议
如果字段本来就是固定范围的 ASCII 标识,可以在入口明确限制字符集,并让数据库的比较规则与该范围配套。展示名称通常不适合这样限制,因为真实姓名或内容标题可能包含多种文字。把显示文字与稳定内部编号分开,往往更容易管理身份和搜索。
若确实需要更广的大小写折叠,可以评估应用生成独立比较键,或使用经过明确配置的扩展及自定义排序规则。无论选择哪一种,都必须说明语言、版本、规范化和冲突策略。简单调用小写转换不一定等价于完整大小写折叠,也不等价于按读音或地区习惯排序。
更换规则还可能让以前允许并存的两行突然变成同一个比较键。这是数据迁移问题,不能仅修改查询参数就视为完成。先列出潜在冲突,确定保留、合并或人工审核方式,再让唯一性校验与查询采用同一套稳定规则,才能避免写入与读取互相矛盾。
不要把相邻的文本机制混成同一个开关
字符规范化、大小写折叠、排序和模式匹配分别解决不同问题。本文的等号比较实验不能推出 LIKE 的全部行为,也不能证明不同编码形式的文本都会合并。验证名称系统时,应针对实际使用的运算符各写样本,而不是只测一次等号就宣布搜索正确。
最后还要加入空字符串、前后空格和业务真正允许的字符。NOCASE 不会替你去掉空格,也不会把缺失名称补成有效值;本例用 NOT NULL 负责必填,但若还要求非空,仍需要另加规则。保存原始值与比较规则的说明,能让未来升级后的差异有据可查。


