数据库技术基础系列笔记写到第9篇,终于可以聊点“动手”的内容了。前面几篇我们把关系模型、SQL语法骨架、事务和索引的概念都过了一遍,但概念归概念,真正坐到电脑前建库建表,第一个拦路的问题一定是:这个字段该用什么类型?这段查询该用哪个运算符?两个问题没搞清楚,后面写SQL全是坑。这次我用的是一套真实可装的国产数据库——达梦DM8,把它的数据类型和运算符从头到尾过一遍,附上写法示例和踩坑记录。
这篇笔记适合三类人:一是正在系统学数据库基础的学生,拿达梦当练手数据库;二是做Oracle/MySQL向达梦迁移的工程师,需要快速搞清楚类型对应关系;三是在做国产化适配、写兼容SQL的人。读完你能直接照着建表、写查询,不用再像翻官方文档那样字海茫茫地找答案。
1. 达梦DM8数据类型全景:先认清“出身”
达梦DM8在安装初始化实例的时候,会让你选择兼容模式,常见的是Oracle兼容和MySQL兼容。不管选哪个模式,SQL的大方向都走得通,但数据类型和某些运算符的行为会有细微差别。用个类比:达梦的SQL是一口“普通话”,但允许你说Oracle方言或者MySQL方言。很多人上手报错,不是达梦本身不行,而是拿MySQL的习惯去写Oracle方言下的SQL,两边对不上。
数据类型是建表的第一道工序,选错了后面全表都得改。DM8的数据类型体系整体上偏向Oracle风格,但又吸收了MySQL的一些便利类型,所以不能完全照搬任何一家的文档。
1.1 DM8与Oracle/MySQL的类型对照逻辑
先看一张对照表,这是迁移实战里最常用的一份速查。我把达梦、Oracle、MySQL三家的常用类型放在一起,这样从哪个生态过来都能一眼找到对应关系。
| 达梦DM8 | Oracle对应 | MySQL对应 | 说明 |
|---|---|---|---|
| NUMBER(p,s) | NUMBER(p,s) | 无直接对应 | 变长数值,按精度标度约束 |
| INT/INTEGER | NUMBER(10) | INT/INTEGER | 普通整数 |
| BIGINT/SMALLINT/TINYINT | 无 | BIGINT等 | 整型族 |
| DECIMAL/NUMERIC | NUMBER | DECIMAL | 定点数,适合金额 |
| FLOAT/DOUBLE | BINARY_FLOAT/BINARY_DOUBLE | FLOAT/DOUBLE | 浮点数 |
| CHAR(n) | CHAR(n) | CHAR(n) | 定长字符串 |
| VARCHAR(n)/VARCHAR2(n) | VARCHAR2(n) | VARCHAR(n) | 变长字符串 |
| CLOB/TEXT | CLOB | TEXT | 大文本 |
| BLOB | BLOB | BLOB | 二进制大对象 |
| DATE | DATE(含时分秒) | DATE(仅到天) | 日期时间 |
| TIMESTAMP(p) | TIMESTAMP(p) | DATETIME | 高精度时间戳 |
| BIT | 无 | BOOLEAN/TINYINT(1) | 布尔/位 |
注意一个最容易被忽略的点:DM8的DATE类型是包含时分秒的,这和Oracle一致,但和MySQL的DATE(只到天)完全不同。从MySQL迁过来的人,常常发现日期字段插进去再查出来多了时间部分,或者查询条件对不上,根源就在这。
1.2 数值类型:精度、标度与选型
达梦的数值类型核心是NUMBER,可以写NUMBER或者NUMBER(p,s)。p叫精度,表示总共有多少位有效数字;s叫标度,表示小数部分保留几位。比如NUMBER(8,2),表示整数部分最多6位,小数2位,总共8位。超过标度时达梦会按四舍五入处理,而不是直接报错,这一点在写入前要有预期。
实际选型我给一套直接抄的经验:状态码、布尔标记用TINYINT或SMALLINT;业务主键用INT或BIGINT,看数据量预估,别省;金额、单价、费率一律用DECIMAL(18,2)这类定点数,绝对不要用FLOAT或DOUBLE存钱。原因很简单,浮点数的二进制表示无法精确表达很多十进制小数,0.1存进去可能变成0.1000000000000000055,单笔看没感觉,累计几百笔误差就出来了。打个比方,浮点数像用勺量水,定点数像用量杯,量杯误差可控。
建表时我习惯把金额精度放大一档,比如DECIMAL(18,4)存储、展示时再四舍五入到2位,这样中间计算环节的误差更小,最后输出的精度也够。还有一个细节,达梦的DECIMAL和NUMBER在绝大多数场景下是同义的,所以写DECIMAL(10,2)和NUMBER(10,2)差别不大,关键是精度要留够。
提示:在达梦的某些严格参数模式下,超精度写入会直接报错而不是四舍五入。建议建表前先确认实例的参数设置,别赌默认行为。
1.3 字符类型:长度单位是字节的坑
达梦的CHAR(n)、VARCHAR(n)、VARCHAR2(n),其中n的单位默认是字节,不是字符数。这一点坑过无数从MySQL迁过来的人。MySQL的VARCHAR(20)是指20个字符,而达梦默认的VARCHAR(20)是20个字节。在UTF-8字符集下,一个汉字占3个字节,VARCHAR(10)只能存3个汉字加1个字节的余量,想存10个汉字必然报“字符串超出长度”之类的错误。
解决办法有两个:一是在初始化实例时把参数LENGTH_IN_CHAR设为1,这样后续定义字符串长度就按字符数计算;二是如果实例已经建好,定义字段时按“预计汉字数乘以3”来预留字节数。我个人更推荐前者,从根上消除换算负担,但要注意这个参数是在初始化数据库实例时确定的,后期改起来比较麻烦。
CHAR和VARCHAR(2)的选择也有讲究。CHAR是定长,存“abc”也会占满10个字节,不足部分补空格;取出时尾部空格通常会被忽略,但参与字符串比较或拼接时容易出意外。VARCHAR2是变长,存多少占多少,绝大多数业务字段都该用它。只有像身份证号、订单号这类长度绝对固定的场景,用CHAR才有意义。
大文本字段方面,CLOB和TEXT在达梦里都能存长文本,区别主要是使用习惯。从Oracle过来就用CLOB,从MySQL过来就用TEXT,两者在达梦的底层映射基本一致。二进制数据用BLOB,比如图片、文件流,直接以二进制形式存储。
1.4 日期时间类型与常用函数
达梦的日期时间类型主要有DATE、TIME、TIMESTAMP(p)、DATETIME。DATE包含年月日时分秒,默认精度到秒;TIMESTAMP可以带小数秒,比如TIMESTAMP(3)精确到毫秒,适合记录流水时间、日志时间。DATETIME在达梦里基本等同TIMESTAMP,写哪个都行。
日期时间相关的函数在实操里用得非常多。最常用的有:SYSDATE取当前数据库时间;TO_DATE把字符串按格式转成日期;TO_CHAR把日期转成指定格式的字符串。写法示例:
SELECT SYSDATE FROM DUAL; -- 当前日期和时间 SELECT TO_DATE('2025-06-01 10:30:00', 'yyyy-mm-dd hh24:mi:ss') FROM DUAL; -- 字符串转日期 SELECT TO_CHAR(SYSDATE, 'yyyy-mm-dd hh24:mi:ss') FROM DUAL; -- 日期转字符串,hh24表示24小时制日期可以直接做加减法,加1就是加一天,减0.5就是减半天。这个特性在做到期日、逾期天数计算时非常顺手。例如查3天前的数据,直接写SYSDATE - 3。格式掩码里的hh24和mm特别容易写反,很多人想取分钟却写了mi,导致永远拿到月份,这个我踩过不只一次。
2. 运算符:从语法规则到执行语义
数据类型解决“存什么”,运算符解决“怎么算”。这一部分看着简单,实际是SQL写错的重灾区。达梦支持算术、比较、逻辑、字符串连接、集合运算等几类运算符,其中NULL与运算符的交互最值得花时间搞清楚。
2.1 算术运算符与除法精度边界
达梦的算术运算符就是标准的加+、减-、乘*、除/,加上取余相关的能力。最常用的场景是金额计算、数量汇总、比率计算。比如:
SELECT price * quantity AS total_amount, price * 0.9 AS discount_price, quantity / 100 AS convert_count FROM t_order_detail;除法的精度行为值得专门说。整数除以整数,在达梦里结果是精确的小数数值,比如1/2得到0.5,不是MySQL旧版本里的0。如果你确实想要整除的效果,需要用FLOOR或者TRUNC处理,而不是指望/自动截断。
除零错误是经典的运行时错误。达梦里除零会报错,SQL执行直接中断。实际开发中,除法的分母经常来自字段值,有时为空有时为0,稳妥做法是用NULLIF把0转成NULL,因为任何数除以NULL的结果是NULL,不会报错:
SELECT price / NULLIF(quantity, 0) FROM t_order_detail; -- quantity为0时结果为NULL,而不是报错取余运算用MOD函数,比如MOD(17, 5)结果为2。达梦在兼容MySQL方言时也支持%写法,但为了跨模式统一,我建议一律写MOD。
2.2 比较运算符与NULL三值逻辑
比较运算符包括=、<>、!=、<、>、<=、>=、BETWEEN AND、IN、LIKE、IS NULL。这些在语法上跟其他数据库没有太大区别,但NULL参与比较时的结果,必须建立正确的心智模型。
SQL的逻辑不是二值逻辑,而是三值逻辑:TRUE、FALSE、UNKNOWN。NULL参与任何比较运算,结果都是UNKNOWN。比如NULL = NULL的结果是UNKNOWN,NULL > 5的结果也是UNKNOWN。WHERE子句只保留结果为TRUE的行,所以写了WHERE name = NULL,会一条数据都查不出来,因为所有行的比较结果都是UNKNOWN,不是FALSE,更不是TRUE。
正确地判断NULL只能用IS NULL或IS NOT NULL:
SELECT * FROM t_user WHERE name IS NULL; -- 查name为空的用户NOT IN和NULL的搭配是另一个高频坑。当NOT IN的子查询结果集中包含NULL时,整个条件的结果会变成UNKNOWN,导致一条数据都查不到。比如:
SELECT * FROM t_user WHERE dept_id NOT IN (SELECT dept_id FROM t_dept); -- 如果t_dept.dept_id存在NULL,这个查询结果为空原因是NOT IN等价于一连串的AND条件:dept_id <> 值1 AND dept_id <> 值2 ...,一旦其中某个值碰上是NULL,AND条件里出现UNKNOWN,整体就无法为TRUE。解决办法是用NOT EXISTS替代,或者先过滤掉子查询里的NULL。这是我强烈建议记在本子上的一个经验,生产环境里因为这个查不出数据的问题,排查成本往往很高。
LIKE运算符也要说一下。%匹配任意多个字符,匹配单个字符。如果模式本身包含%或,需要用ESCAPE指定转义符,比如LIKE '100_%' ESCAPE '',表示匹配以“100_”开头的字符串。实际业务里,模糊搜索最多的就是LIKE '%关键字%',看起来简单,但要注意通配符开头的写法会导致索引失效,数据量大时性能会很难看。
2.3 逻辑运算符AND OR NOT与短路判断
逻辑运算符用在WHERE、HAVING、CASE WHEN等条件里,优先级从高到低是NOT、AND、OR。这个优先级顺序看似简单,实际经常写错。比如WHERE a = 1 OR a = 2 AND b = 3,实际执行的是a = 1 OR (a = 2 AND b = 3),而不是(a = 1 OR a = 2) AND b = 3。为了避免歧义,复杂的逻辑条件一定加括号,括号是不要钱的,可读性却值钱得多。
AND、OR与NULL组合的结果,是SQL逻辑里最容易绕晕的地方,这里直接给一张真值表:
| 左值 | 右值 | AND结果 | OR结果 |
|---|---|---|---|
| TRUE | TRUE | TRUE | TRUE |
| TRUE | FALSE | FALSE | TRUE |
| TRUE | NULL | NULL | TRUE |
| FALSE | NULL | FALSE | NULL |
| NULL | NULL | NULL | NULL |
记法很简单:AND碰到FALSE就是FALSE,OR碰到TRUE就是TRUE,其它情况遇到NULL就是NULL。
所谓短路,是指逻辑运算在结果已经确定时不再计算后面的表达式。SQL标准里没有强制规定表达式求值顺序,数据库优化器可能调整执行计划,所以千万不要依赖短路去保护某个函数的合法性。比如写了CASE WHEN n > 0 THEN 1/n ELSE 0 END,理论上应该安全,但实际执行时不一定按你想象的顺序评估子表达式,更稳妥的写法是CASE WHEN n = 0 THEN 0 ELSE 1/n END,把除法安全兜住。
2.4 字符串连接与其他补充运算符
字符串连接运算符在达梦里是双竖线||,例如SELECT first_name || ' ' || last_name FROM t_user。这是Oracle风格的写法,也是达梦默认推荐的写法。从MySQL过来的人可能习惯CONCAT函数,达梦也支持CONCAT和CONCAT_WS,但在Oracle兼容模式下,||最通用。
集合运算也是SQL里一类特殊的运算符:UNION、UNION ALL、MINUS/EXCEPT、INTERSECT。UNION去重,UNION ALL不去重,需要全量合并时优先用UNION ALL,避免无谓的排序去重开销。MINUS和INTERSECT在达梦里都可以用,语义和其他数据库一致。
位运算在达梦里也有支持,包括&、|、^、~、<<、>>这类位运算符,但要注意兼容模式的影响。Oracle风格下没有那么多现成的位运算符,通常用BITAND函数处理按位与;MySQL兼容模式下使用&和|更顺手。实际业务里直接用位运算的场景不多,大多是标志位设计时用到,这里知道有这回事就行,不用深挖。
3. 类型转换与兼容模式实操
类型转换是“数据类型”和“运算符”两块内容的交叉点。SQL里比较和计算时,两边的类型不一致怎么办?数据库要么自动转,要么报错。搞清达梦的转换规则,才能避免一堆莫名其妙的运行时错误。
3.1 显式转换函数与隐式转换规则
达梦的显式类型转换有三种常用手段:CAST、CONVERT、TO系列函数。CAST是通用写法,语法清晰:CAST(expr AS target_type)。CONVERT的功能类似,但参数顺序在不同数据库里有差异,跨库移植时要留心。TO_CHAR、TO_NUMBER、TO_DATE则是把值往特定类型转的专用函数。
真实项目里最常用的还是TO系列,因为带格式控制。TO_CHAR可以把数字转成指定格式的字符串,比如TO_CHAR(12345.6, '999,999.99');TO_DATE可以把字符串按指定日期格式解析;TO_NUMBER则把字符串转成数字,比如TO_NUMBER('123.45')。
隐式转换是数据库自动完成的类型转换。比如数字字段和字符串比较时,达梦会尝试把字符串转成数字再比;字符串和日期比较时,会尝试把字符串解析成日期。隐式转换的优点是省事,缺点是埋雷。最典型的问题:字段上有索引,但WHERE条件的写法触发了隐式转换,导致索引失效,全表扫描。比如字符串类型的字段code,你写WHERE code = 123456,数据库会把每行的code转成数字去比较,索引就用不上了。正确的写法是WHERE code = '123456',保持类型一致。
我的经验是:能用显式转换的地方就不要依赖隐式转换,写SQL时把类型对齐,既能保护性能,也能让语义更清晰。
3.2 兼容模式下写SQL的差异
达梦兼容Oracle和MySQL两套主流方言,这个能力在做国产化替代时非常实用。但“兼容”不等于“完全一致”,实际使用中要留意差异,避免写出换一个模式就跑不了的SQL。
Oracle兼容模式下,可以用DUAL表、SYSDATE、||字符串连接、ROWNUM伪列,这些是Oracle开发者的习惯。MySQL兼容模式下,LIMIT分页、反引号引用标识符、AUTO_INCREMENT自增列这些写法可以用。
我建议在项目里确定一个主兼容模式,所有SQL按这个模式的规范来写,不要混用。比如开发团队大部分来自MySQL背景,那初始化实例时就选MySQL兼容,日常写LIMIT、AUTO_INCREMENT都顺畅;如果团队来自Oracle背景,就选Oracle兼容,写ROWNUM和DUAL更顺手。最忌讳的是今天写LIMIT明天写ROWNUM,同一个项目里两种分页风格并存,后面维护的人会崩溃。
分页查询这块我多说一句:Oracle兼容模式下的标准分页是三层嵌套子查询配ROWNUM,写起来繁琐但稳定;MySQL兼容模式下直接LIMIT offset, count。达梦的新版本也支持了一些简化的分页语法,但统一按主兼容模式的规范写最稳。
3.3 用Navicat连接DM8与实践验证
光看语法不够,落地验证一下。我用Navicat连接达梦DM8,把数据类型和运算符过了一遍,这里直接给连接步骤和验证SQL。
Navicat连接达梦,在连接对话框里选择“达梦”类型,主机填127.0.0.1,端口填达梦默认的5236,用户名用SYSDBA,密码是安装实例时设置的。如果Navicat提示缺少驱动,需要在工具菜单的驱动管理里添加达梦JDBC驱动包。驱动从达梦官网下载,是一个DmJdbcDriver18.jar之类的文件,添加后重启连接就正常了。
连接成功后,建一张覆盖多种类型的测试表:
CREATE TABLE t_practice ( id INT IDENTITY(1,1) PRIMARY KEY, name VARCHAR2(50), age TINYINT, score DECIMAL(6,2), birthday DATE, create_time TIMESTAMP DEFAULT SYSDATE );注意IDENTITY是达梦的自增列写法,建表时不需要手动插id值。插入几条数据:
INSERT INTO t_practice (name, age, score, birthday) VALUES ('张三', 25, 88.50, TO_DATE('2000-03-15', 'yyyy-mm-dd')); INSERT INTO t_practice (name, age, score, birthday) VALUES ('李四', 30, 76.00, TO_DATE('1995-08-20', 'yyyy-mm-dd'));查询时把运算符都用上,验证一下行为:
SELECT id, name, age + 1 AS next_age, score * 0.9 AS discount_price, TO_CHAR(birthday, 'yyyy-mm-dd') AS birthday_str FROM t_practice WHERE age >= 18 AND score > 70 ORDER BY score DESC;这条SQL覆盖了算术运算符、比较运算符、逻辑运算符、TO_CHAR转换和ORDER BY排序,跑通之后数据类型和运算符的基础应用就算入门了。想验证表达式正确性,可以用DUAL直接算:
SELECT 1 + 1 AS r1, MOD(17, 5) AS r2, 'a' || 'b' AS r3 FROM DUAL;结果分别是2、2、ab,一目了然。
4. 常见问题与排查实录
实操中遇到的问题,比语法本身更能加深理解。我把这段时间在达梦上反复遇到的典型问题整理成速查表,再挑三个最典型的案例拆开讲,每个都是真实场景,定位思路和解决方案都可以直接复用。
4.1 高频报错速查表
| 报错现象 | 典型原因 | 处理方法 |
|---|---|---|
| 字符串超长/数据过长 | VARCHAR长度按字节计算,中文占多字节 | 预留字节数,或把LENGTH_IN_CHAR设为1 |
| 无效的数字/无法从字符串转换为数字 | 隐式转换时字符串里混了非数字字符 | 用TO_NUMBER显式转换,先清洗数据 |
| 除数不能为0 | 算术运算除数为0 | 用NULLIF(denominator, 0)规避 |
| WHERE条件查不出数据 | 写了= NULL或NOT IN带NULL子查询 | 改用IS NULL,或把NOT IN换NOT EXISTS |
| 日期格式无效 | TO_DATE的格式掩码与实际字符串不匹配 | 统一用yyyy-mm-dd hh24:mi:ss这类标准掩码 |
| 列名或表名找不到 | 引号、大小写敏感导致标识符不匹配 | 建表时统一大小写,字符串常量和标识符分清 |
| 主键冲突 | 自增列使用不当或重复插入 | 检查IDENTITY设置,确认插入时不手填id |
4.2 三个容易踩坑的真实案例
第一个案例是WHERE name = NULL。SQL执行没报错,但结果集是空的。很多初学者误以为NULL是“空值”,用=去判断,看着合理,实则完全错误。正确写法是IS NULL。这个案例在培训课堂上每次都有不少人中招,核心还是没建立起三值逻辑的心智模型。
第二个案例是NOT IN配合子查询结果集。表t_user里有500条记录,执行WHERE dept_id NOT IN (SELECT dept_id FROM t_dept)却返回0行,而t_dept里明明只有3个部门,500条记录应该大部分都查得出来才对。排查后发现t_dept.dept_id列里有NULL值,整个条件就被NULL污染了。改成NOT EXISTS或者先在子查询里过滤掉NULL,问题立刻解决。这个案例的价值在于:报错不是唯一的问题形态,查不出数据更隐蔽。
第三个案例是VARCHAR2(10)存中文超长。从MySQL迁移的同事习惯性写VARCHAR2(10),然后往里插“数据库基础”这五个字,直接报字符串超长。UTF-8下一个汉字3字节,5个汉字要15字节,10不够用。这不是达梦的问题,是长度单位没搞清楚。同一个字段按字符算改成VARCHAR2(15)或者初始化时打开LENGTH_IN_CHAR,问题就没了。
4.3 排查思路与工具方法
报错之后的排查顺序,我建议先看错误码和错误文案,达梦的错误信息里通常直接带了原因,比如“无效的数值”指向类型转换,“数值超出范围”指向精度问题。文字不够清楚时,去达梦安装目录下的日志目录里翻实例日志,里面会记录更详细的上下文。
SQL层面的排查有个小技巧:把复杂查询拆成最小表达式,用DUAL一个个验证。比如怀疑隐式转换有问题,就单独执行SELECT TO_NUMBER('123abc') FROM DUAL,直接复现错误,比抱着整条大SQL猜快得多。性能问题则用EXPLAIN看执行计划,确认是否触发全表扫描、有没有用上索引。
调试期间建议和数据打交道时保留现场:把出错的SQL、参数、表结构都留档,方便对照排查。很多时候问题不是单一原因,比如既字段长度不够、又有类型隐式转换、还有NULL参与计算,三个因素叠加,一层层拆开才看得清楚。
这套笔记整理下来,我最大的体会是:达梦DM8并不是另一套完全陌生的东西,它的数据类型骨架和Oracle高度相似,运算符体系也符合SQL标准,真正让人栽跟头的往往就集中在字节长度、NULL语义、隐式转换和兼容模式这几个点上。刚开始学的人容易按MySQL的习惯写,导致类型不对、NULL判断出错;迁移过来的人又容易忽略字节与字符的差异,在中文数据上反复踩坑。把类型对照表、运算符优先级和NULL处理的规则打印出来贴在显示器旁边,写上一个月,这些坑基本就都能绕开了。