1. 项目概述SQLite 的“类型自由”不是没规矩而是换了一套更聪明的规矩刚接触 SQLite 的人常被它那句“动态类型系统”搞晕——明明建表时写了INTEGER、TEXT结果插入个字符串到整数字段里居然不报错甚至还能正常查出来这哪是数据库简直是宽容度拉满的收容所。我第一次在生产环境里遇到一个user_id INTEGER字段里存了abc123查询时用WHERE user_id 123居然查不到但WHERE user_id 123却能命中当场就懵了。后来才明白SQLite 根本没在玩传统数据库那套“强类型约束”的把戏它用的是存储类Storage Class这套底层机制而我们写的INTEGER、TEXT这些只是“亲和性Affinity”——说白了就是给 SQLite 一个温和的建议“你最好按这个路子来存”但它真不听劝也完全不拦着。这种设计不是偷懒而是为嵌入式场景量身定制的没有独立服务进程、不占内存、启动快、零配置所有数据都塞进一个文件里。它要的是极致轻量和鲁棒性而不是 Oracle 那种严丝合缝的类型校验。所以“SQLite Data Types: A Close Look at Storage Classes”这个标题核心不在教你怎么写CREATE TABLE而在于帮你掀开表层语法的盖子看清底下那套真正决定数据怎么落盘、怎么比较、怎么转换的存储类引擎。它解决的问题非常具体为什么CAST(123 AS INTEGER)能成功而CAST(abc AS INTEGER)却返回 0为什么两个看起来都是数字的字段做JOIN时性能差得离谱为什么导出 CSV 再导入后原本的整数变成了带引号的字符串再查询就失效了。这篇文章适合三类人一是正在用 SQLite 做本地缓存、移动端 App 数据库或 IoT 设备存储的开发者你们每天都在和它打交道但可能还没摸清它的脾气二是刚从 MySQL/PostgreSQL 转过来被“类型宽松”搞得怀疑人生的后端工程师三是想深入理解数据库底层行为的数据工程师因为 SQLite 的存储类机制其实是关系型数据库类型系统的一个极简、透明、可验证的范本。它不藏私所有规则都写在官方文档里而且你随时可以用typeof()函数亲眼看到每个值的真实存储类。2. 存储类与亲和性的双轨制SQLite 类型系统的底层逻辑2.1 五大存储类数据在磁盘上真正的“身份证”SQLite 不像其他数据库那样把类型信息作为元数据硬编码在表结构里。它只认五种最原始的“存储类Storage Class”这五种就是数据在.db文件里落盘时的唯一身份标识。它们不是抽象概念而是实实在在影响字节布局、序列化方式和比较逻辑的物理分类。我拿一个简单的test.db文件在十六进制编辑器里直接打开就能看到不同存储类对应的不同前缀字节这就是最硬核的证据。NULL就代表空值。它不占用任何有效数据空间只用一个字节的类型标记0x05来标识。这是最轻量的存储类也是唯一一个没有“值”的类。INTEGER存储一个有符号的 64 位整数-2^63 到 2^63-1。注意它不是INT或BIGINT的别名而是一个确定的、固定的二进制格式。当你插入123、-456、9223372036854775807它们最终都以小端序的 8 字节二进制形式躺在磁盘上。我做过测试插入1和插入1000000000000000000在文件大小上没有任何区别都是 8 字节加上类型标记。REAL存储一个 64 位 IEEE 浮点数。同样它是物理格式不是FLOAT或DOUBLE的模糊指代。3.14159、1e-5、-2.71828全部按标准浮点规则编码。这里有个关键细节SQLite不支持NaN非数字或无穷大Inf如果你试图插入1e999它会静默地变成Inf但 SQLite 会把它当作一个非法值并将其转换为0.0。这是很多数值计算场景下容易踩的坑。TEXT存储一个 UTF-8 编码的字符串。重点是“UTF-8”不是VARCHAR或CLOB。它没有长度限制理论上实际大小只受可用磁盘空间约束。字符串内容本身是变长的前面会有一个长度前缀varint 编码然后才是 UTF-8 字节流。这意味着hello和你好后者是 6 个 UTF-8 字节在存储上是完全不同的结构。我曾用hexdump查看过一个纯 ASCII 字符串的存储效率极高而一个包含大量中文的 TEXT 字段其 B-tree 页面会更快地被填满间接影响查询性能。BLOB存储一个“二进制大对象”。它就是一个纯粹的、未经解释的字节数组。SQLite 对它不做任何编码、解码或转换。X48656C6C6F即Hello的十六进制表示和readfile(image.jpg)返回的结果都以原始字节流的形式存储。这是唯一一个 SQLite 绝对“不碰”的类型也是处理图片、加密密钥、序列化协议缓冲区protobuf等二进制数据的唯一正道。提示你可以用typeof()函数实时查看任意表达式的存储类。例如SELECT typeof(123), typeof(123), typeof(3.14), typeof(NULL), typeof(X0102);会分别返回integer,text,real,null,blob。这是调试类型问题的第一步比看建表语句管用一百倍。2.2 亲和性Affinity建表时写的那些类型名其实只是“温柔的建议”现在我们明白了数据在磁盘上只有那五种存储类。那么CREATE TABLE users (id INTEGER, name TEXT, score REAL, avatar BLOB);这条语句里的INTEGER、TEXT等到底在起什么作用答案是它们定义了该列的“亲和性Affinity”。亲和性不是强制约束而是一种类型转换偏好。当你要向一列插入一个值时SQLite 会先看这个值本身的存储类再看目标列的亲和性然后根据一套明确的规则决定是否以及如何进行隐式转换。SQLite 官方定义了五种亲和性TEXT、NUMERIC、INTEGER、REAL和NONE。其中INTEGER、REAL、TEXT、BLOB、NUMERIC这些常见的类型名都会被映射到对应的亲和性。比如INTEGER、INT、TINYINT、SMALLINT、MEDIUMINT、BIGINT、UNSIGNED BIG INT、INT2、INT8→INTEGER亲和性TEXT、CLOB、CHARACTER、NCHAR、NATIVE CHARACTER、VARYING CHARACTER、NVARCHAR→TEXT亲和性REAL、DOUBLE、DOUBLE PRECISION、FLOAT→REAL亲和性NUMERIC、DECIMAL、BOOLEAN、DATE、DATETIME→NUMERIC亲和性BLOB以及未指定类型的列→NONE亲和性这个映射表非常重要因为它解释了为什么你写score DECIMAL(5,2)SQLite 并不会真的去检查小数位数它只是给了score列一个NUMERIC亲和性后续的转换规则就由这个亲和性主导。注意NUMERIC亲和性是整个系统里最“中庸”的一个。它既不偏向整数也不偏向浮点而是根据输入值的“外观”来决定最终的存储类。如果一个字符串长得像整数如123它会转成INTEGER如果长得像浮点如123.45它会转成REAL如果长得不像数字如abc它就老老实实存成TEXT。这正是NUMERIC亲和性最强大也最危险的地方——它太智能了智能到有时会违背你的直觉。2.3 亲和性转换规则一张表看懂所有隐式转换理解了存储类和亲和性下一步就是掌握它们相遇时的“化学反应”。SQLite 的转换规则非常清晰且全部公开。我把官方规则浓缩成一张实用速查表这张表是我调试无数个“类型不匹配” Bug 后总结出来的精华目标列亲和性插入值的存储类转换结果最终存储类实际案例与说明INTEGERTEXT如果文本能无损解析为整数如123,-456则转为INTEGER否则保持TEXT。INSERT INTO t(col) VALUES(123)→INTEGER;INSERT INTO t(col) VALUES(123.45)→TEXT因为.45无法无损转整;INSERT INTO t(col) VALUES(abc)→TEXTINTEGERREAL如果浮点数是整数如123.0,-456.0则转为INTEGER否则保持REAL。INSERT INTO t(col) VALUES(123.0)→INTEGER;INSERT INTO t(col) VALUES(123.45)→REALREALTEXT如果文本能解析为数字整数或浮点则转为REAL否则保持TEXT。INSERT INTO t(col) VALUES(123)→REAL123.0;INSERT INTO t(col) VALUES(123.45)→REAL;INSERT INTO t(col) VALUES(abc)→TEXTTEXTINTEGER/REAL/BLOB一律不转换保持原存储类。这是TEXT亲和性最霸道的一点。INSERT INTO t(col) VALUES(123)→INTEGER不是TEXTINSERT INTO t(col) VALUES(123.45)→REAL。TEXT亲和性只对TEXT输入“有求必应”对其他输入“爱答不理”。NUMERICTEXT尝试解析为数字能转整数则INTEGER能转浮点则REAL否则TEXT。INSERT INTO t(col) VALUES(123)→INTEGER;INSERT INTO t(col) VALUES(123.45)→REAL;INSERT INTO t(col) VALUES(123.0)→INTEGER因为.0是整数;INSERT INTO t(col) VALUES(abc)→TEXTNONE任意绝不转换原样存储。BLOB列或未声明类型的列。INSERT INTO t(col) VALUES(123)→INTEGER;INSERT INTO t(col) VALUES(123)→TEXT。它就像一个透明的管道。这张表的核心逻辑是亲和性只对TEXT输入“积极工作”对其他输入基本“躺平”。这也是为什么你在INTEGER列里能塞进字符串却很难在TEXT列里把数字自动变成字符串——因为TEXT亲和性根本懒得干这事。我曾经在一个日志分析脚本里把时间戳1672531200一个整数直接插进一个created_at TEXT列结果查询时用WHERE created_at 1672531200居然不生效因为比较是按字符串字典序进行的2 1672531200为真。最后只能全量UPDATE把它CAST成TEXT才解决问题。这个教训让我牢牢记住TEXT亲和性不是“保证存成文本”而是“保证不对非文本输入动手”。3. 核心实操环节从建表到查询每一步都藏着类型陷阱3.1 建表阶段如何用亲和性“引导”而非“强迫”数据很多人以为建表时选对类型名就万事大吉了。但 SQLite 的哲学是“引导”而非“强迫”。所以建表的关键不在于“我要什么类型”而在于“我希望数据以什么方式被使用”。我分享三个经过实战检验的建表策略。策略一用NUMERIC亲和性处理“混合输入”的业务字段假设你有一个price字段前端传来的可能是字符串19.99也可能是数字19.99还可能是错误的free。你希望它尽可能存成数字方便后续计算和排序但又不能因为一个free就让整条记录失败。这时NUMERIC就是你的最佳选择。-- ✅ 推荐用 NUMERIC 亲和性兼容性最强 CREATE TABLE products ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, price NUMERIC -- 注意这里写 NUMERIC不是 REAL 或 DECIMAL ); -- 插入各种形态的数据 INSERT INTO products (name, price) VALUES (Laptop, 1299.99); -- 存为 REAL INSERT INTO products (name, price) VALUES (Mouse, 29.99); -- 存为 REAL INSERT INTO products (name, price) VALUES (Free Trial, free); -- 存为 TEXT INSERT INTO products (name, price) VALUES (Discount, 99); -- 存为 INTEGER -- 查询时可以安全地用 COALESCE 和 CAST 处理 SELECT name, CASE WHEN typeof(price) IN (integer, real) THEN price ELSE 0.0 END AS numeric_price FROM products ORDER BY numeric_price DESC;这个方案的优势在于它把类型转换的“决策权”交给了 SQLite 最成熟的规则引擎而不是自己在应用层写一堆if-else去判断字符串格式。NUMERIC亲和性就像一个经验丰富的老会计看到1299.99就知道该记成一笔浮点支出看到free就知道该记成一笔特殊备注。策略二用TEXT亲和性“锁死”字符串语义杜绝意外转换有些字段其“字符串”属性本身就是业务逻辑的一部分。比如order_id订单号、sku_code商品编码、phone_number电话号码。这些值虽然看起来像数字但你永远不应该对它们做数学运算。如果用INTEGER亲和性00123会被转成123前导零丢失123-456-789会被转成123因为-之后的部分被忽略这完全是灾难。-- ✅ 推荐用 TEXT 亲和性确保“所见即所得” CREATE TABLE orders ( id INTEGER PRIMARY KEY, order_id TEXT NOT NULL, -- 关键必须是 TEXT customer_name TEXT ); -- 插入带前导零和分隔符的 ID INSERT INTO orders (order_id, customer_name) VALUES (00123, Alice); INSERT INTO orders (order_id, customer_name) VALUES (123-456-789, Bob); -- 查询时必须用字符串比较 SELECT * FROM orders WHERE order_id 00123; -- ✅ 正确 SELECT * FROM orders WHERE order_id 00123; -- ❌ 错误00123 是整数会触发 typeof(order_id)text vs 123 的比较结果为 false我曾经维护过一个电商系统sku_code字段最初用了VARCHAR(50)在 SQLite 里等同于TEXT亲和性一切安好。后来一个新同事觉得“sku_code都是数字用INTEGER更省内存”于是改成了INTEGER。上线后所有带字母的 SKU如ABC123都变成了0库存同步直接崩盘。这个血泪教训告诉我当一个字段的“字符”含义比“数值”含义更重要时TEXT亲和性是唯一的、不可妥协的选择。策略三用NONE亲和性BLOB存储“不可解释”的二进制数据对于加密后的数据、序列化的 JSON 对象、图像缩略图等你绝对不希望 SQLite 对其做任何解释。BLOB亲和性就是为此而生。-- ✅ 推荐用 BLOB 亲和性保证字节级保真 CREATE TABLE user_profiles ( id INTEGER PRIMARY KEY, encrypted_password BLOB NOT NULL, -- 关键BLOB 亲和性 avatar_thumbnail BLOB ); -- 插入时必须用十六进制字面量或参数绑定 INSERT INTO user_profiles (encrypted_password) VALUES (X2a626364313233); -- abcd123 的 hex -- 查询时得到的仍是原始字节 SELECT typeof(encrypted_password), length(encrypted_password) FROM user_profiles; -- 结果blob | 7 7 个字节一字不差这里有个重要技巧在应用代码中插入BLOB时永远不要用字符串拼接的方式如INSERT INTO ... VALUES ( hexString )这极易引发 SQL 注入。务必使用预编译语句Prepared Statement和?占位符让驱动程序负责正确的二进制绑定。3.2 查询与比较阶段类型如何无声地主宰你的查询结果建表只是开始查询时的类型行为才真正决定你的数据是否“可信”。SQLite 的比较操作符,!,,和ORDER BY子句其行为完全取决于参与比较的两个值的实际存储类而不是它们的列亲和性。这是我踩过最多坑的地方。陷阱一混合存储类的比较结果出人意料-- 假设有一个表price 列是 NUMERIC 亲和性 CREATE TABLE items (name TEXT, price NUMERIC); INSERT INTO items VALUES (Apple, 1.99); -- 存为 REAL INSERT INTO items VALUES (Banana, 1.99); -- 存为 REAL INSERT INTO items VALUES (Cherry, 2); -- 存为 INTEGER INSERT INTO items VALUES (Date, 2); -- 存为 INTEGER -- 看似相同的查询结果却不同 SELECT * FROM items WHERE price 2; -- 返回 Cherry 和 Date两个 INTEGER SELECT * FROM items WHERE price 2; -- 返回 Cherry 和 Date2 被转为 INTEGER SELECT * FROM items WHERE price 2.0; -- 返回 Cherry 和 Date2.0 被转为 INTEGER SELECT * FROM items WHERE price 2.0; -- 只返回 Cherry因为 2.0 被转为 REAL而 Cherry 是 INTEGERREAL 和 INTEGER 在 SQLite 中是**不相等**的这个例子揭示了一个残酷真相2INTEGER和2.0REAL在 SQLite 里是两个完全不同的值它们的存储类不同因此2 2.0的结果是false。这和 Python 或 JavaScript 的宽松相等完全不同。我在写一个价格区间筛选功能时就因为前端传来的min_price是字符串10.0而后端 SQL 里写的是WHERE price ?结果10.0被当成了REAL而数据库里存的10是INTEGER导致10 10.0为false10这个价格被错误地过滤掉了。解决方案只有一个在应用层统一将所有数值输入CAST成你需要的类型或者在 SQL 里显式CAST。陷阱二ORDER BY的排序逻辑由存储类决定CREATE TABLE scores (name TEXT, value NUMERIC); INSERT INTO scores VALUES (Alice, 100); -- INTEGER INSERT INTO scores VALUES (Bob, 99.5); -- REAL INSERT INTO scores VALUES (Charlie, 99); -- INTEGER INSERT INTO scores VALUES (Diana, 99.0); -- REAL SELECT name, value, typeof(value) FROM scores ORDER BY value; -- 结果顺序是Alice(100), Charlie(99), Diana(99.0), Bob(99.5) -- 注意Charlie(99, INTEGER) 排在 Diana(99.0, REAL) 前面因为 INTEGER REAL 的比较规则是先比存储类INTEGER 优先级高于 REAL所以 99 99.0 为 trueSQLite 的排序规则是先按存储类的优先级排序再按值排序。其存储类优先级顺序是NULLINTEGERREALTEXTBLOB。所以一个INTEGER99 永远小于一个REAL99.0哪怕它们的数学值相等。这在做排行榜时是个隐形炸弹。我的解决方案是在ORDER BY时强制统一类型-- ✅ 安全的排序全部转成 REAL 进行比较 SELECT name, value FROM scores ORDER BY CAST(value AS REAL); -- 或者全部转成 TEXT如果需要字典序 SELECT name, value FROM scores ORDER BY CAST(value AS TEXT);陷阱三GROUP BY和DISTINCT的“相等”判定GROUP BY和DISTINCT的行为和比较完全一致。这意味着100INTEGER和100.0REAL会被认为是两个不同的组。SELECT DISTINCT value, typeof(value) FROM scores; -- 结果会是两行100 (integer) 和 100.0 (real)即使它们的数学值相同。这在做数据清洗和报表统计时会导致重复计数。我处理过一个用户行为日志表event_duration字段是NUMERIC日志来源多样有的写30有的写30.0结果COUNT(DISTINCT event_duration)总是比实际多。最终我不得不在 ETL 流程里加了一步SELECT DISTINCT CAST(event_duration AS REAL) FROM logs。3.3 类型转换函数CAST、typeof和0的实战用法SQLite 提供了几个核心的类型操作函数它们是绕过亲和性、精确控制类型行为的利器。typeof(expression)这是你的“X光机”。在任何复杂的查询里第一件事就是把它加进去看看数据到底是什么存储类。SELECT name, price, typeof(price) FROM items; -- 立刻暴露哪些是 INTEGER哪些是 REAL哪些是 TEXTCAST(expression AS type)这是你的“手术刀”。type只能是TEXT、INTEGER、REAL、NUMERIC或BLOB。CAST是强制转换它会严格按照规则执行失败时返回默认值如CAST(abc AS INTEGER)返回0。-- 安全地将 price 转为数字用于计算 SELECT SUM(CAST(price AS REAL)) FROM items; -- 将所有 price 格式化为统一的字符串用于展示 SELECT name, printf(%.2f, CAST(price AS REAL)) FROM items;0、||等隐式转换技巧这是老手的快捷键。0会尝试将任何值转为REAL||字符串连接空字符串会尝试将任何值转为TEXT。它们比CAST短但在复杂表达式中可读性差我只在简单场景用。-- 快速转数字等价于 CAST(x AS REAL) SELECT * FROM items WHERE price 0 50; -- 快速转字符串等价于 CAST(x AS TEXT) SELECT * FROM items WHERE price || LIKE 1%;实操心得在写任何涉及数值计算、范围查询或聚合的 SQL 之前我一定会先运行一个SELECT ... , typeof(col) FROM ... LIMIT 5来“探路”。这5行数据往往就能暴露出你表里潜藏的类型混乱。我见过太多团队花几天时间 debug 一个“查询结果不对”的 Bug最后发现只是因为某个字段里混进了几个TEXT类型的0而其他都是INTEGER的0导致SUM()计算时0被当成了0但COUNT(*)却把0当作一个有效行造成了统计偏差。这个习惯帮我节省了至少上百小时的无效排查时间。4. 常见问题与避坑指南来自真实战场的排错笔记4.1 “为什么我的整数查询不返回结果”——WHERE子句的类型迷宫问题现象一个user_id INTEGER的表插入了123但SELECT * FROM users WHERE user_id 123能查到SELECT * FROM users WHERE user_id 123却查不到。根因分析这几乎总是由列亲和性与查询值存储类不匹配造成的。user_id列如果是TEXT亲和性比如你建表时写了user_id VARCHAR(10)那么插入的123会被存为INTEGER但查询时123是TEXTINTEGER和TEXT在 SQLite 中是不相等的。反之如果user_id是INTEGER亲和性而你插入的是123字符串它会被转成INTEGER123那么WHERE user_id 123和WHERE user_id 123都能查到因为123在比较时也会被转成INTEGER。快速诊断-- 第一步确认列的亲和性 PRAGMA table_info(users); -- 看 type 列推断亲和性 -- 第二步确认数据的实际存储类 SELECT user_id, typeof(user_id) FROM users LIMIT 5; -- 第三步确认查询值的存储类 SELECT typeof(123), typeof(123); -- 结果text | integer解决方案长期方案修改建表语句确保亲和性与业务语义一致。ID 必须是INTEGER亲和性。短期方案在查询中显式CAST。-- ✅ 强制将查询值转为列的预期类型 SELECT * FROM users WHERE user_id CAST(123 AS INTEGER); -- 或者更通用的写法适用于多种输入源 SELECT * FROM users WHERE CAST(user_id AS TEXT) 123;4.2 “为什么SUM()的结果是0”——CAST失败的静默陷阱问题现象一个amount NUMERIC的表SELECT SUM(amount) FROM transactions总是返回0但SELECT * FROM transactions显示数据明明存在。根因分析SUM()是一个聚合函数它要求所有参与聚合的值都必须是数字类型INTEGER或REAL。如果amount列里混入了TEXT类型的非数字字符串如N/A,pending,SUM()在遇到第一个非数字TEXT时会将其CAST为0并继续累加。但如果所有值都是TEXT且都无法转为数字SUM()就会返回0。这是一个完全静默的失败没有任何警告。快速诊断-- 找出所有非数字的 amount SELECT amount, typeof(amount) FROM transactions WHERE typeof(amount) ! integer AND typeof(amount) ! real; -- 或者找出 CAST 失败的值 SELECT amount FROM transactions WHERE CAST(amount AS INTEGER) 0 AND amount ! 0 AND amount ! 0;解决方案数据清洗在插入前应用层严格校验拒绝非数字字符串。SQL 层防护在聚合前用CASE过滤掉坏数据。SELECT SUM( CASE WHEN typeof(amount) IN (integer, real) THEN amount WHEN typeof(amount) text AND amount GLOB [0-9]* THEN CAST(amount AS INTEGER) ELSE 0 END ) FROM transactions;4.3 “为什么导出再导入后数据‘变形’了”——CSV 导入导出的类型失真问题现象用.dump或.output导出 SQLite 表为 SQL 文件再用.read导入或者用sqlite3 db .dump | sqlite3 new.db迁移发现原本的整数123变成了字符串123再查询就失效了。根因分析.dump命令生成的是INSERT INTO ... VALUES(...)语句它会把所有值都用单引号括起来即VALUES(123, John)。当 SQLite 解析这条语句时123是一个TEXT字面量它会遵循目标列的亲和性规则进行转换。如果目标列是INTEGER亲和性它会转成INTEGER但如果目标列是TEXT亲和性它就永远是TEXT。而.dump本身不包含建表语句中的类型信息它只包含CREATE TABLE语句而CREATE TABLE语句里的类型名又会被 SQLite 重新映射为亲和性。所以迁移过程中的类型失真根源在于.dump的“文本化”本质。解决方案最佳实践用.mode insert和.separator导出为纯数据# 导出为 INSERT 语句带类型 sqlite3 old.db .mode insert users .output users_insert.sql SELECT * FROM users; # 这会生成 INSERT INTO users VALUES(123, John); —— 数字不带引号 # 导出为 CSV无类型 sqlite3 old.db .mode csv .output users.csv SELECT * FROM users; # 导入 CSV 时指定列类型 sqlite3 new.db .mode csv .import users.csv users终极保障在迁移脚本中用PRAGMA table_info动态生成CAST语句。这很重但对于金融级数据迁移是必要的。4.4 “为什么JSON_EXTRACT的结果不能直接比较”——JSON 函数与存储类的交互问题现象CREATE TABLE logs (data TEXT); INSERT INTO logs VALUES({status: 200});然后SELECT JSON_EXTRACT(data, $.status) FROM logs返回200但WHERE JSON_EXTRACT(data, $.status) 200不生效。根因分析JSON_EXTRACT函数返回的值其存储类是TEXT即使它提取的是一个 JSON 数字。这是为了保证 JSON 的“无损性”——JSON_EXTRACT({num: 123.45}, $.num)必须返回123.45TEXT而不是123.45REAL否则就丢失了原始的精度信息。所以你得到的是一个TEXT类型的200而不是INTEGER类型的200。解决方案-- ✅ 正确显式 CAST SELECT * FROM logs WHERE CAST(JSON_EXTRACT(data, $.status) AS INTEGER) 200; -- ✅ 或者用 JSON_TYPE 辅助判断 SELECT * FROM logs WHERE JSON_TYPE(data,