一、为什么要关注数据类型的选择

在达梦数据库里搞开发,数据类型这件事看着不起眼,但实际上踩坑的人特别多。很多人写SQL的时候随手就用了VARCHAR(500),或者所有数值都往NUMBER里扔,结果到了项目后期,查询变慢、存储爆满、数据转换报错,找问题找得头大。

今天这篇文章,我就用大白话把达梦数据库里数据类型该怎么选、跨类型转换常见什么问题、怎么处理,这些内容一次讲清楚。不管你是刚接触达梦的新手,还是用了好几年的老手,都能从中找到对自己有用的东西。

1.1 一个实际踩坑的例子

想象这样一个场景:你有一张订单表,订单编号字段你用VARCHAR(100)存的。后来业务说订单编号最多30个字符,但你懒得改。等到数据量到了上亿条的时候,你才发现光这一个字段就多存了几十G的空间,查询索引也变大了,性能自然上不去。

更糟的是,当你想拿这个订单编号去和其他系统的NUMBER类型字段做关联查询的时候,达梦数据库得先把VARCHAR转成NUMBER再比较,这个隐式转换的过程在大数据量下就是个性能杀手。

1.2 数据类型选错了会怎样

数据类型选得不合适,通常会带来这几个问题:

第一,存储浪费。比如你用VARCHAR(1000)存手机号,每个手机号才11位,剩下989个字符的空间全是浪费。乘以千万级数据量,这浪费可不是小数目。

第二,性能下降。类型太大、太模糊,索引效率低,查询计划也选不优。数据库优化器在生成执行计划的时候,对类型更明确的字段能做出更好的判断。

第三,转换报错。跨类型转换如果没处理好,轻则数据截断,重则直接报错让业务中断。

第四,跨系统不兼容。不同系统、不同数据库对同一类型的理解可能不同,后续数据迁移或者对接的时候会非常头疼。

二、达梦数据库常用数据类型一览

达梦数据库(DM Database)是国产数据库的代表,它的数据类型体系跟Oracle有比较多相似之处,但也有自己的特色。下面我按类别把常用的类型整理一下,方便大家心里有个底。

2.1 字符类型

字符类型是最常用的,用来存名字、地址、描述这些文本内容。达梦数据库里主要有以下几种:

CHAR:定长字符类型,不足指定长度的会用空格补齐。适合存固定长度的内容,比如身份证前两位、手机号区号之类的。

VARCHAR:变长字符类型,实际存多少算多少,不浪费空间。这是最常用的字符类型,大多数文本字段都应该用这个。

NVARCHAR:Unicode字符类型的变长版本,专门用来存中文、日文、韩文等多语言内容。如果你的系统涉及国际化,这个类型少不了。

CLOB:大文本对象,用来存超长文本,比如文章正文、日志内容这些。比VARCHAR大得多,适合存几十K甚至几M的文本。

BLOB:二进制大对象,存图片、文件、加密数据这些二进制内容。

2.2 数值类型

NUMBER:通用数值类型,可以存整数也可以存小数,精度可以指定。这是最灵活的数值类型。

INTEGER:整数类型,不存小数。比NUMBER更适合存纯整数场景。

DECIMAL / NUMERIC:精确小数类型,精度和标度都可以指定。金额、税率这类对精度要求高的场景要用这个。

FLOAT:浮点数类型,适合科学计算,但不适合存金额,因为浮点计算有精度损失。

2.3 日期时间类型

DATE:日期类型,只存年月日,不存时分秒。

DATETIME:日期时间类型,存年月日时分秒。

TIMESTAMP:时间戳类型,精度可以指定到纳秒级,适合对时间精度要求极高的场景。

2.4 其他类型

BOOLEAN:布尔类型,只能存TRUE、FALSE或者NULL。

ROWID:行标识符,达梦数据库内部用的,一般不手动指定。

下面用一段完整的建表语句来把这些类型串起来看:

-- 技术栈:DM (达梦数据库) SQL
-- 创建一张包含各种常见数据类型的示例表
CREATE TABLE DEMO_USER_INFO
(
    USER_ID         NUMBER(10)          NOT NULL,    -- 用户ID,10位整数,不允许为空
    USER_NO         VARCHAR(20)         NOT NULL,    -- 用户编号,最长20个字符
    USER_NAME       NVARCHAR(50)        NOT NULL,    -- 用户姓名,支持中文,最长50个字符
    PHONE           CHAR(11)            NULL,         -- 手机号,固定11位
    EMAIL           VARCHAR(100)        NULL,         -- 邮箱地址,最长100个字符
    BALANCE         DECIMAL(12, 2)      DEFAULT 0,    -- 账户余额,12位总长度,2位小数
    AGE             INTEGER             NULL,         -- 年龄,整数
    REG_DATE        DATE                DEFAULT SYSDATE, -- 注册日期,默认当前日期
    LOGIN_TIME      TIMESTAMP           NULL,         -- 最后登录时间,高精度时间戳
    AVATAR_DATA     BLOB                NULL,         -- 头像二进制数据
    BIO             CLOB                NULL,         -- 个人简介,大文本
    IS_ACTIVE       BOOLEAN             DEFAULT TRUE  -- 是否激活
);
-- 为用户ID创建主键索引
CREATE INDEX IDX_USER_ID ON DEMO_USER_INFO(USER_ID);

这段建表语句里,我刻意选了不同类型来展示各自的适用场景。比如手机号用CHAR(11)是因为中国手机号固定11位,用定长类型查起来稍微快一点。余额用DECIMAL(12,2)是因为涉及到钱,精度不能丢。注册时间用DATE就够了,不需要精确到秒。

三、数据类型选择的核心原则

知道有哪些类型之后,关键是怎么选。下面我总结几条最实用的选择原则,每条都配上具体例子说明。

3.1 够用就好,别贪大

这是最基本的原则。字段能存多大内容,你就指定多大长度,别为了"以后可能用"就一口气指定个几百几千的。

拿邮箱字段来说,Gmail、Outlook这些邮箱地址通常不会超过100个字符。你就算给VARCHAR(500),实际也浪费空间。但如果业务确认邮箱地址最长不超过200个字符,那VARCHAR(200)就够了。

-- 技术栈:DM (达梦数据库) SQL
-- 反例:邮箱字段给了不必要的超长长度
CREATE TABLE BAD_DESIGN
(
    ID      NUMBER(10),
    EMAIL   VARCHAR(500)    -- 邮箱地址很少超过200个字符,500纯属浪费
);

-- 正例:合理指定长度
CREATE TABLE GOOD_DESIGN
(
    ID      NUMBER(10),
    EMAIL   VARCHAR(200)    -- 200个字符足够覆盖绝大多数邮箱地址
);

这里有个细节要注意:VARCHAR和NVARCHAR的长度单位不一样。VARCHAR的长度单位是字节,NVARCHAR的长度单位是字符。一个中文在达梦数据库里通常占3个字节(UTF-8编码),所以VARCHAR(30)最多存10个中文字符,而NVARCHAR(30)可以存30个中文字符。这个区别在存中文的时候一定要搞清楚。

3.2 数值精度按需指定

NUMBER类型可以指定精度和标度。精度是总位数,标度是小数点后的位数。不指定的话,达梦会用默认值,可能不是你想要的。

-- 技术栈:DM (达梦数据库) SQL
-- 示例:不同精度指定的效果对比
CREATE TABLE NUMBER_DEMO
(
    -- 不指定精度标度:达梦默认处理,可能不符合预期
    COL_NO_SPEC   NUMBER,
    
    -- 指定精度为10,标度为2:最多10位总长度,2位小数
    COL_SPEC_10_2 NUMBER(10, 2),
    
    -- 纯整数:指定精度为10,不指定标度(默认为0)
    COL_INT_10    NUMBER(10),
    
    -- 高精度小数:19位总长度,6位小数,适合金融计算
    COL_DECIMAL   DECIMAL(19, 6)
);

-- 插入测试数据
INSERT INTO NUMBER_DEMO VALUES (9999999999.99, 1234567890.12, 9999999999, 0.123456);
INSERT INTO NUMBER_DEMO VALUES (12345678901.12, 1234567890.12, 12345678901, 0.123456789);

金额的字段建议用DECIMAL类型,不要用FLOAT。因为FLOAT是浮点表示,计算0.1加0.2可能不等于0.3,金额差一分钱在财务系统里就是大问题。

3.3 中文内容用NVARCHAR

如果你的表里要存中文,直接用VARCHAR存中文虽然技术上能存(UTF-8编码下),但容易遇到长度计算混乱的问题。最稳妥的做法是:中文内容用NVARCHAR,纯英文用VARCHAR。

-- 技术栈:DM (达梦数据库) SQL
-- 创建一张包含中英文混合内容的表
CREATE TABLE CONTENT_TABLE
(
    ID          NUMBER(10)          NOT NULL,
    TITLE_EN    VARCHAR(200)        NULL,         -- 英文标题
    TITLE_CN    NVARCHAR(100)       NULL,         -- 中文标题
    DESC_EN     VARCHAR(2000)       NULL,         -- 英文描述
    DESC_CN     NVARCHAR(1000)      NULL          -- 中文描述
);

-- 插入测试数据
INSERT INTO CONTENT_TABLE VALUES (
    1,
    'Hello World',                          -- 英文标题
    '你好世界',                              -- 中文标题,NVARCHAR存储清晰
    'This is an English description.',       -- 英文描述
    '这是一个中文描述,用于测试NVARCHAR类型的存储能力。'  -- 中文描述
);

-- 查询验证
SELECT ID, TITLE_EN, TITLE_CN, 
       LENGTH(TITLE_EN) AS EN_LEN,            -- VARCHAR按字节计长度
       LENGTH(TITLE_CN) AS CN_LEN,            -- NVARCHAR按字符计长度
       LENGTHB(TITLE_CN) AS CN_BYTE_LEN       -- 获取字节长度
FROM CONTENT_TABLE;

LENGTH函数对NVARCHAR返回的是字符数,对VARCHAR返回的是字节数。LENGTHB函数返回字节数。搞清楚这个区别,你在做字符串处理的时候就不会踩坑。

3.4 日期时间精度匹配业务

DATE类型只存日期不存时间,如果你需要存"注册时间"这种需要精确到秒甚至毫秒的场景,用DATE就不够了。这时候应该用DATETIME或者TIMESTAMP。

-- 技术栈:DM (达梦数据库) SQL
-- 日期时间类型的精度对比
CREATE TABLE TIME_DEMO
(
    ID          NUMBER(10)        NOT NULL,
    CREATION_DATE   DATE            DEFAULT SYSDATE,   -- 创建日期,只要年月日
    MODIFIED_TIME   DATETIME        NULL,               -- 修改时间,精确到秒
    AUDIT_TIMESTAMP TIMESTAMP       NULL                -- 审计时间戳,精确到纳秒
);

-- 插入数据
INSERT INTO TIME_DEMO VALUES (
    1,
    SYSDATE,                          -- 当前日期
    SYSDATE,                          -- 当前时间
    SYSDATE                           -- 当前时间戳
);

-- 查询对比
SELECT 
    ID,
    CREATION_DATE,                    -- 显示日期
    MODIFIED_TIME,                    -- 显示日期时间
    AUDIT_TIMESTAMP                   -- 显示时间戳
FROM TIME_DEMO;

四、跨类型数据转换的常见问题与处理

选好了类型只是第一步,实际开发中跨类型数据转换才是真正容易出问题的地方。达梦数据库会做一些隐式转换,但有时候这个"自动帮忙"反而会带来麻烦。

4.1 隐式转换的陷阱

达梦数据库在查询时会做一些隐式转换。比如你用VARCHAR类型的字段和NUMBER类型的值做比较,数据库会先把VARCHAR转成NUMBER再比。这个过程在数据量小的时候感觉不出来,到了大数据量就是性能灾难。

-- 技术栈:DM (达梦数据库) SQL
-- 隐式转换导致索引失效的经典问题

-- 创建测试表和索引
CREATE TABLE ORDERS
(
    ORDER_NO    VARCHAR(30)    NOT NULL,   -- 订单号,VARCHAR类型
    AMOUNT      DECIMAL(12, 2) NOT NULL    -- 金额
);
CREATE INDEX IDX_ORDER_NO ON ORDERS(ORDER_NO);

-- 插入测试数据
INSERT INTO ORDERS VALUES ('2024010100001', 199.99);
INSERT INTO ORDERS VALUES ('2024010100002', 299.99);
INSERT INTO ORDERS VALUES ('2024010100003', 399.99);

-- 问题查询:用NUMBER类型比较VARCHAR字段
-- 达梦会隐式转换,导致索引失效
SELECT * FROM ORDERS WHERE ORDER_NO = 2024010100001;

-- 正确查询:类型匹配,索引生效
SELECT * FROM ORDERS WHERE ORDER_NO = '2024010100001';

注意上面的区别:第一个查询用的是数字2024010100001,没有加引号,达梦会先对ORDER_NO做隐式转换,索引就废了。第二个查询加了引号,类型匹配,索引能正常用上。

用EXPLAIN可以查看执行计划,验证索引有没有生效:

-- 技术栈:DM (达梦数据库) SQL
-- 查看执行计划,确认索引使用情况

-- 查看正确查询的执行计划
EXPLAIN SELECT * FROM ORDERS WHERE ORDER_NO = '2024010100001';
-- 结果中应该看到 INDEX RANGE SCAN 或者类似索引扫描操作

-- 查看错误查询的执行计划
EXPLAIN SELECT * FROM ORDERS WHERE ORDER_NO = 2024010100001;
-- 结果中可能看到 FULL TABLE SCAN,说明全表扫描,索引没生效

4.2 显式转换的正确用法

当隐式转换不可控或者性能不可接受的时候,应该用显式转换函数。达梦数据库提供了几种常用的转换函数:

-- 技术栈:DM (达梦数据库) SQL
-- 显式转换函数使用示例

-- TO_CHAR:将数值或日期转换为字符类型
SELECT 
    TO_CHAR(ORDER_NO, 'FM00000000000') AS ORDER_NO_PADDED,   -- 补零到11位
    TO_CHAR(AMOUNT, 'FM99,999.99')      AS AMOUNT_FORMATTED   -- 格式化金额
FROM ORDERS;

-- TO_NUMBER:将字符转换为数值类型
-- 示例:把字符型的电话号码转成数字(假设都是纯数字)
SELECT TO_NUMBER('13800138000') AS PHONE_NUM;

-- TO_DATE:将字符转换为日期类型
SELECT 
    TO_DATE('2024-01-15', 'YYYY-MM-DD')         AS DATE1,    -- 标准日期格式
    TO_DATE('20240115093015', 'YYYYMMDDHH24MISS') AS DATE2   -- 自定义格式
;

-- TO_DATE和TO_CHAR配合使用,做日期格式化输出
SELECT 
    SYSDATE                                     AS TODAY,
    TO_CHAR(SYSDATE, 'YYYY年MM月DD日')          AS TODAY_CN,   -- 中文格式
    TO_CHAR(SYSDATE, 'Day, Month DD, YYYY')     AS TODAY_EN   -- 英文格式
FROM DUAL;

4.3 转换失败的常见场景

显式转换虽然可控,但处理不当还是会出问题。最常见的就是数据里混入了不可转换的内容。

-- 技术栈:DM (达梦数据库) SQL
-- 转换失败的场景分析

-- 场景1:字符中包含非数字内容,转NUMBER失败
-- 假设有一个表,部分电话号码前面带了区号
CREATE TABLE USER_CONTACTS
(
    ID      NUMBER(10)      NOT NULL,
    PHONE   VARCHAR(30)     NULL    -- 电话号码,部分带区号
);

INSERT INTO USER_CONTACTS VALUES (1, '13800138000');
INSERT INTO USER_CONTACTS VALUES (2, '+86-13900139000');   -- 带区号和特殊字符
INSERT INTO USER_CONTACTS VALUES (3, NULL);

-- 错误操作:直接转换会报错
-- SELECT TO_NUMBER(PHONE) FROM USER_CONTACTS;
-- 报错:转换失败(对应Oracle的ORA-01425类似错误)

-- 安全做法:先用正则过滤,只转换合法的纯数字
SELECT 
    ID,
    PHONE,
    CASE 
        WHEN PHONE IS NULL THEN NULL
        WHEN REGEXP_LIKE(PHONE, '^[0-9]+$') THEN TO_NUMBER(PHONE)
        ELSE NULL    -- 不满足条件的返回NULL,不报错
    END AS PHONE_NUM
FROM USER_CONTACTS;

上面的例子展示了处理转换失败的标准模式:先用条件判断(CASE WHEN或者REGEXP_LIKE),确认数据可转换之后再转,不可转换的给个默认值(NULL或0)。

4.4 字符集相关的转换问题

达梦数据库的字符集设置也会影响转换行为。如果你的数据库字符集和客户端字符集不一致,存中文的时候可能乱码或者长度不对。

-- 技术栈:DM (达梦数据库) SQL
-- 检查当前字符集设置

-- 查看数据库字符集
SELECT 
    NAME,
    VALUE    AS CHARACTER_SET    -- 字符集名称
FROM V$NLS_PARAMETERS
WHERE NAME IN ('NLS_CHARACTERSET', 'NLS_NCHAR_CHARACTERSET');

如果发现字符集不匹配,需要检查连接配置。达梦客户端连接参数里可以指定字符集:

-- 技术栈:DM (达梦数据库) SQL
-- 在SQL会话中临时切换字符集(需要权限)
-- ALTER DATABASE SET NLS_CHARACTERSET 'UTF-8';

-- 验证字符集切换后的存储效果
INSERT INTO DEMO_USER_INFO (USER_ID, USER_NAME) VALUES (99999, '张三李四五六七八九');
SELECT USER_ID, USER_NAME, 
       LENGTH(USER_NAME) AS CHAR_LEN, 
       LENGTHB(USER_NAME) AS BYTE_LEN
FROM DEMO_USER_INFO
WHERE USER_ID = 99999;

五、应用场景与技术优缺点

5.1 典型应用场景

理解了数据类型选择和转换的原理之后,来看几个实际的开发场景:

场景一:金融系统对账模块

金融系统的金额字段必须用DECIMAL类型,精度通常设为DECIMAL(19,6)甚至更高。因为对账过程中涉及多个系统的数据合并,每个系统的精度可能不同,用DECIMAL可以确保不丢精度。同时在跨系统对账时,要用显式转换统一精度标准:

-- 技术栈:DM (达梦数据库) SQL
-- 金融系统对账:跨系统金额精度统一处理
CREATE TABLE TRANSACTION_RECORDS
(
    TXN_ID      NUMBER(20)          NOT NULL,    -- 交易流水号
    SOURCE_SYS  VARCHAR(20)         NOT NULL,    -- 来源系统
    AMOUNT_A    DECIMAL(19, 6)      NULL,        -- 系统A的金额
    AMOUNT_B    DECIMAL(19, 4)      NULL,        -- 系统B的金额(精度不同)
    AMOUNT_SUM  DECIMAL(20, 6)      NULL,        -- 合计金额(取更高精度)
    MATCH_FLAG  BOOLEAN             DEFAULT FALSE -- 是否匹配
);

-- 对账查询:显式统一精度后比较
SELECT 
    TXN_ID,
    SOURCE_SYS,
    AMOUNT_A,
    -- 将系统B的金额显式提升到6位小数精度
    TO_DECIMAL(AMOUNT_B, 19, 6)  AS AMOUNT_B_STD,
    (AMOUNT_A + AMOUNT_B)       AS AMOUNT_SUM,
    CASE 
        WHEN AMOUNT_A = TO_DECIMAL(AMOUNT_B, 19, 6) THEN TRUE
        ELSE FALSE 
    END AS MATCH_FLAG
FROM TRANSACTION_RECORDS;

场景二:电商平台订单查询

电商平台的订单表通常数据量很大,字段类型选择直接决定查询性能。订单号用VARCHAR存,订单时间用DATETIME存,商品描述用NVARCHAR存。关键点在于避免隐式转换导致索引失效:

-- 技术栈:DM (达梦数据库) SQL
-- 电商订单查询:避免隐式转换保证性能
CREATE TABLE EC_ORDERS
(
    ORDER_ID      VARCHAR(32)          NOT NULL,     -- 订单ID,VARCHAR存字符型
    ORDER_TIME    DATETIME             NOT NULL,      -- 下单时间
    USER_ID       NUMBER(12)           NOT NULL,      -- 用户ID
    PRODUCT_NAME  NVARCHAR(200)        NULL,          -- 商品名称,中文
    AMOUNT        DECIMAL(10, 2)       NOT NULL,      -- 订单金额
    STATUS        VARCHAR(10)          DEFAULT 'PAID' -- 订单状态
);

-- 创建联合索引,覆盖常用查询条件
CREATE INDEX IDX_EC_QUERY 
ON EC_ORDERS(ORDER_TIME, STATUS, USER_ID);

-- 正确查询:所有类型匹配
SELECT ORDER_ID, ORDER_TIME, PRODUCT_NAME, AMOUNT 
FROM EC_ORDERS 
WHERE ORDER_TIME >= DATETIME'2024-01-01 00:00:00'
  AND STATUS = 'PAID'
  AND USER_ID = 123456789012;
  -- 注意:ORDER_ID如果也要查询条件,必须用字符串'...'而不是数字

-- 错误查询示例:隐式转换
-- SELECT * FROM EC_ORDERS WHERE USER_ID = '123456789012';
-- 这里用字符串去比较NUMBER字段,会触发隐式转换

5.2 各类型的优缺点对比

VARCHAR vs CHAR:VARCHAR灵活、省空间,但索引效率比CHAR稍低。CHAR定长,比较操作可能稍快,但浪费空间。如果字段内容长度固定(比如邮编、区号),用CHAR合适;长度可变就用VARCHAR。

NUMBER vs INTEGER:NUMBER更灵活,能存小数也能存整数,精度可控。INTEGER只存整数,存储更紧凑,比较运算可能更快。如果确定是整数且不需要小数部分,用INTEGER。

DATE vs DATETIME vs TIMESTAMP:DATE最省空间但精度最低。DATETIME精度到秒,大多数业务够用。TIMESTAMP精度到纳秒,适合对时间要求极高的场景,但存储开销最大。

CLOB vs VARCHAR:CLOB能存超大文本,但查询性能差,不能直接用WHERE条件过滤。VARCHAR有长度上限但查询灵活。如果文本内容可能超过几K,用CLOB;否则用VARCHAR。

5.3 跨类型转换技术的优缺点

隐式转换的优点:写SQL方便,不用每次都手动写转换函数。达梦数据库自动帮你转,对新手友好。

隐式转换的缺点:性能不可控,特别是索引失效的问题在大数据量下非常致命。而且有时候转换结果不符合预期,比如字符串转日期可能因为字符集问题导致格式解析错误。

显式转换的优点:完全可控,转换逻辑透明,性能可优化。出问题时也容易排查。

显式转换的缺点:写起来麻烦,SQL语句更长。如果业务逻辑里有很多转换,代码会变得臃肿。

六、注意事项与最佳实践

6.1 设计阶段的注意事项

在表设计阶段就要把数据类型想清楚,别等数据量上来再改。几个关键注意点:

第一,字段长度要基于业务需求来定,不要拍脑袋。可以跟产品经理确认字段的实际取值范围,然后留20%到30%的余量就够了。

第二,中文内容统一用NVARCHAR,不要VARCHAR和NVARCHAR混用。混用会导致排序、比较的行为不一致,查询结果可能出乎意料。

第三,金额类字段统一用DECIMAL,精度标准全库统一。不要一张表用DECIMAL(12,2),另一张表用DECIMAL(10,4),对账的时候会很痛苦。

第四,时间字段要区分清楚。业务日期用DATE,带时间的操作用DATETIME,需要高精度的用TIMESTAMP。不要所有时间都用TIMESTAMP,没必要。

6.2 开发阶段的注意事项

开发写SQL的时候,有几个习惯可以帮你避开很多坑:

第一,字符串值一定要加引号。WHERE ID = 123 和 WHERE ID = '123' 如果ID是VARCHAR类型,后者才不会触发隐式转换。

第二,写跨类型比较的SQL之前,先确认两边的类型是否一致。不一致的话,用显式转换处理,别指望隐式转换。

第三,用EXPLAIN查看执行计划,养成习惯。特别是涉及索引的查询,一定要确认索引生效了。

第四,批量数据导入的时候,提前做数据清洗和类型验证。不要指望达梦帮你处理脏数据,转换失败可能直接中断整个批次。

-- 技术栈:DM (达梦数据库) SQL
-- 批量导入前的数据验证示例
-- 假设从临时表导入正式表

-- 创建临时表接收原始数据
CREATE GLOBAL TEMPORARY TABLE TMP_IMPORT_DATA
(
    RAW_ID      VARCHAR(50)     NULL,   -- 原始ID,字符类型
    RAW_NAME    VARCHAR(200)    NULL,   -- 原始名称
    RAW_AGE     VARCHAR(10)     NULL,   -- 原始年龄,字符类型
    RAW_SALARY  VARCHAR(30)     NULL    -- 原始薪资,字符类型
);

-- 导入验证:先检查数据质量
SELECT 
    COUNT(*) AS TOTAL_ROWS,
    -- 检查ID是否可转为NUMBER
    COUNT(CASE WHEN REGEXP_LIKE(RAW_ID, '^[0-9]+$') THEN 1 END) AS VALID_ID_COUNT,
    -- 检查年龄是否在合理范围
    COUNT(CASE WHEN REGEXP_LIKE(RAW_AGE, '^[0-9]+$') 
               AND TO_NUMBER(RAW_AGE) BETWEEN 0 AND 150 THEN 1 END) AS VALID_AGE_COUNT,
    -- 检查薪资是否可转
    COUNT(CASE WHEN REGEXP_LIKE(RAW_SALARY, '^[0-9]+\\.?[0-9]*$') THEN 1 END) AS VALID_SALARY_COUNT
FROM TMP_IMPORT_DATA;

-- 验证通过后,用显式转换导入正式表
INSERT INTO DEMO_USER_INFO (USER_ID, USER_NAME, AGE, BALANCE)
SELECT 
    TO_NUMBER(RAW_ID),
    RAW_NAME,
    TO_NUMBER(RAW_AGE),
    TO_DECIMAL(RAW_SALARY, 12, 2)
FROM TMP_IMPORT_DATA
WHERE REGEXP_LIKE(RAW_ID, '^[0-9]+$')
  AND REGEXP_LIKE(RAW_AGE, '^[0-9]+$')
  AND TO_NUMBER(RAW_AGE) BETWEEN 0 AND 150
  AND REGEXP_LIKE(RAW_SALARY, '^[0-9]+\\.?[0-9]*$');

6.3 性能优化的注意事项

数据类型和转换对性能的影响主要体现在索引效率和查询计划上。几个优化要点:

第一,避免在WHERE条件中对字段做函数转换。比如WHERE TO_NUMBER(PHONE) = 13800138000 会导致索引失效。应该反过来写:WHERE PHONE = TO_CHAR(13800138000) 或者直接用字符串比较。

第二,索引列的类型要和查询条件的类型一致。建了VARCHAR(30)的索引,查询条件就用VARCHAR值,别用NUMBER。

第三,对于经常做跨类型转换的字段,可以考虑加一个计算列或者冗余字段。比如经常要把VARCHAR类型的订单号转NUMBER比较,可以额外建一个NUMBER类型的冗余字段存转换后的值。

-- 技术栈:DM (达梦数据库) SQL
-- 性能优化:添加冗余字段避免频繁转换
-- 假设经常需要按订单号的数字部分排序

-- 原始表(只有VARCHAR的订单号)
CREATE TABLE ORDERS_OPT
(
    ORDER_NO    VARCHAR(30)       NOT NULL,     -- 原始订单号
    ORDER_SEQ   NUMBER(15)        NULL,          -- 冗余字段:顺序号
    AMOUNT      DECIMAL(12, 2)    NOT NULL
);

-- 从订单号中提取数字部分作为顺序号
INSERT INTO ORDERS_OPT (ORDER_NO, ORDER_SEQ, AMOUNT)
SELECT 
    ORDER_NO,
    TO_NUMBER(REGEXP_SUBSTR(ORDER_NO, '[0-9]+$')),  -- 提取尾部数字
    AMOUNT
FROM ORDERS;

-- 在冗余字段上建索引
CREATE INDEX IDX_ORDER_SEQ ON ORDERS_OPT(ORDER_SEQ);

-- 现在按数字顺序排序不用做转换了
SELECT * FROM ORDERS_OPT ORDER BY ORDER_SEQ;

七、文章总结

数据类型选择不是小事,它在表设计阶段决定了后续的性能上限和数据质量。达梦数据库提供了丰富的数据类型,选对类型需要结合业务场景来考虑。核心原则就是:够用就好、精度匹配、中文用NVARCHAR、时间精度按需选。

跨类型数据转换的问题处理,关键在于理解隐式转换的风险,养成显式转换的习惯。隐式转换虽然写起来省事,但在生产环境下可能带来性能问题和数据异常。显式转换虽然多写几行代码,但换来的是可控性和可维护性。

最后给几个最实用的建议:

第一,表设计的时候认真选类型,别全用VARCHAR和NUMBER兜底。类型选对了,后续的查询优化、数据迁移都会轻松很多。

第二,写SQL的时候多一个心眼,留意类型是否匹配。一个引号的区别,可能就是全表扫描和索引扫描的区别。

第三,数据导入和转换的时候做充分的数据验证。宁可多花一分钟验证,不要等批量导入失败了再排查。

第四,定期用EXPLAIN检查关键查询的执行计划。确认索引在生效,没有隐藏的隐式转换在消耗性能。

达梦数据库在数据类型和转换方面的设计思路和主流数据库有共通之处,但也有很多自己的特色。掌握了选择原则和处理方法,在国产数据库的开发道路上会少走很多弯路。