上周帮一个做信创改造的团队看环境,开发跑过来说建表一直报错,我登上去一看,用户是建了,但默认表空间还挂在 MAIN 上,索引和数据文件全挤在一个 128M 的文件里,扩容也没做规划。这种情况我遇到过太多次了,问题不出在 SQL 语法上,而是建库之初没把"用户、表空间、模式"这三件事当成一个整体来设计。达梦 DM8 在这块跟很多人熟悉的 MySQL 差别不小,MySQL 里CREATE DATABASE一把梭的思维,搬到达梦上会连着踩坑。
这篇内容我打算把 DM8 从实例初始化、表空间规划、建用户、建表结构到数据导入导出这一整条链路讲透,重点放在那些官方文档里一笔带过、但实际部署时天天要面对的参数和细节,比如页大小、字符集、大小写敏感这几个"一次定终身"的设置,还有 dexp/dimp 导入时那个把人折腾得够呛的 PG_GBK 与 PG_UTF8 编码问题。适合刚开始接触达梦的运维和开发,也适合从 Oracle 迁移过来、想知道两者差异的老手参考。
1. 建用户这件事,为什么在DM8里要先谈表空间和模式
1.1 新手的典型翻车路径
大部分人的操作顺序是这样的:装完数据库,用 SYSDBA 登进去,直接敲一句CREATE USER APP IDENTIFIED BY "App@12345";,然后GRANT DBA TO APP;,觉得搞定了。接着用 APP 登录建表,发现表建到了 MAIN 表空间里,跑了几天业务量上来,MAIN 表空间告警,再回头想把表挪走,就得挨个ALTER TABLE ... MOVE TABLESPACE ...,中间还要处理索引失效、约束重建的问题。
这个坑的根源在于 DM8 的默认行为:CREATE USER如果不显式指定DEFAULT TABLESPACE,用户对象会落到实例初始化时设定的默认表空间(通常是 MAIN)。而 MAIN 表空间在 dminit 初始化时只给了 128M,虽然开了自动扩展,但默认的扩展上限往往也不够生产环境用。
第二个容易忽略的点是模式。DM8 里用户和模式是强绑定的关系,执行CREATE USER的同时系统会自动创建一个与用户名同名的模式。这一点和 Oracle 一致,但和 MySQL 的"库"概念完全不同。很多从 MySQL 转过来的人会习惯性再执行一次CREATE SCHEMA APP,结果直接报"模式已存在"。更麻烦的是,如果业务代码里的 SQL 硬编码了APP.T_ORDER这种带模式前缀的写法,后面想改用户名就等于要改一遍代码。
我的建议是:用户名的命名规则在项目启动阶段就固定下来,一旦确定就不要动。名字本身用什么无所谓,APP_BIZ、BIZ_USER都行,但得统一,比如按业务域划分,一个业务域一个用户,不要一个库塞十几种业务。
1.2 DM8自带的四个表空间各自干什么
初始化一个全新实例之后,DM8 会自动创建几个系统表空间,这块必须搞清楚,不然后面排查空间问题会一头雾水。
| 表空间名 | 用途 | 能否删除 | 备注 |
|---|---|---|---|
| SYSTEM | 存放数据字典、系统表和系统视图 | 否 | 空间不足会直接导致库不可用 |
| ROLL | 存放回滚记录 | 否 | 大事务会撑爆它,需要重点监控 |
| TEMP | 临时表空间,排序、哈希连接用 | 否 | 复杂查询多的话要加大 |
| MAIN | 默认用户表空间 | 理论可删但不建议 | 大小默认 128M |
| HMAIN | HUGE 表(列存)专用 | 可 | 用不到列存可以不关注 |
实际项目里我一般这么分配:系统相关的全部保持默认,业务数据建独立的表空间,索引再单独建一个,如果有列存分析需求,HMAIN 单独扩容。为什么要数据文件和索引文件分开?两个原因,一是随机 IO 和顺序 IO 的访问模式不同,分开放在不同物理盘上能减少争抢;二是某张表数据损坏需要恢复时,索引可以重建,不用一起恢复,省时间。
顺便说说热词里提到的 DW 和 DSC 的区别,因为这直接影响到表空间的规划思路。DSC(达梦共享存储集群)是多个实例挂同一份共享存储上的数据文件,类似 Oracle RAC,这时候表空间的每个数据文件都必须放在共享存储上,且所有节点看到的路径要一致。而 DW(数据守护)是一主多备,主库写、备库重做日志,硬件和数据文件都是独立的,表空间规划按单机来就行。这两者在建表空间时的差别,DSC 要额外注意数据文件必须落在共享盘、不能加本地盘路径,DW 则要留意主备两边的目录结构保持一致,否则切换后 dm.ini 里的路径对不上。
2. 实例初始化:dminit阶段就要定死的参数
2.1 一次定终身的四个参数
DM8 的PAGE_SIZE、EXTENT_SIZE、CHARSET、LENGTH_IN_CHAR、CASE_SENSITIVE、BLANK_PAD_MODE这几个参数,一旦初始化完成就不能修改,只能删库重建。这不是危言耸听,我见过为了改字符集把整个库导出再重建的案例,前后折腾了两天。
先说PAGE_SIZE,数据页大小,可选 4K、8K、16K、32K。OLTP 场景建议 16K,OLAP 场景建议 32K。为什么?页越大,单次 IO 读的数据越多,对大表扫描、统计分析有利;但页越大,小事务写入时的写放大也越明显,OLTP 场景下 32K 的页会浪费不少空间。达梦官方文档里说初次使用建议 32K,但我个人的实践是,如果业务系统事务量大、单行数据小,16K 更划算。
CHARSET只有 0(GB18030)和 1(UTF-8)两个常用选项。2024 年了,除非你有明确的国标对接需求,否则一律选 UTF-8。这个后面导入导出那节会重点展开。
LENGTH_IN_CHAR这个参数特别容易被忽略,但它的影响面极大。取值 1 时,VARCHAR(10)表示 10 个字符;取值 0 时,表示 10 个字节。UTF-8 下一个汉字占 3 个字节,也就是说,如果设成 0,VARCHAR(10)只能存 3 个汉字,第 4 个就报截断错误。更坑的是,如果建表时没有意识到这一点,后期想改就意味着所有涉及长度的字段类型都要重新评估。
CASE_SENSITIVE建议保持 1(大小写敏感)。虽然设成 0 会让迁移时更省事,但达梦很多系统视图、系统函数的名字是区分大小写的,设成 0 有时反而会引发一些难以定位的问题。
参数速查表:
| 参数 | 可选值 | 推荐值 | 能否修改 |
|---|---|---|---|
| PAGE_SIZE | 4/8/16/32 | OLTP 16,OLAP 32 | 否 |
| EXTENT_SIZE | 16/32(页) | 16 | 否 |
| CHARSET | 0=GB18030,1=UTF-8 | 1 | 否 |
| LENGTH_IN_CHAR | 0/1 | 1 | 否 |
| CASE_SENSITIVE | 0/1 | 1 | 否 |
| BLANK_PAD_MODE | 0/1 | 1(兼容 Oracle) | 否 |
BLANK_PAD_MODE这个参数值得一提,它控制字符串比较和存储时是否对尾部空格做填充处理。从 Oracle 迁移过来的项目建议设 1,行为更接近,否则'abc'和'abc '的比较结果会和原来不一样,这种差异在业务代码里排查起来非常痛苦。
2.2 一份可直接复制的初始化命令与目录规划
假设你已经在 Linux 上装好了 DM8,安装路径是/opt/dmdbms,接下来做实例初始化。先规划目录,我的习惯是按实例名分目录:
mkdir -p /dmdata/DAMENG chown -R dmdba:dinstall /dmdata然后切到 dmdba 用户,执行 dminit:
cd /opt/dmdbms/bin ./dminit PATH=/dmdata DB_NAME=DAMENG \ INSTANCE_NAME=DMSERVER \ PORT_NUM=5236 \ PAGE_SIZE=16 \ EXTENT_SIZE=16 \ CHARSET=1 \ LENGTH_IN_CHAR=1 \ CASE_SENSITIVE=1 \ BLANK_PAD_MODE=1 \ SYSDBA_PWD='Dm@Sysdba2024' \ SYSAUDITOR_PWD='Dm@Auditor2024'PATH是数据文件存放的根目录,DB_NAME是数据库名,会和实例名一起决定数据目录结构。执行完之后会在/dmdata/DAMENG下生成 dm.ini、数据文件、控制文件等。
2.3 服务注册与开机自启
初始化完成后,数据目录里还没有服务。手动启动的话可以./dmserver /dmdata/DAMENG/dm.ini,但生产环境肯定要用服务方式。达梦提供了注册脚本:
./dm_service_installer.sh -t dmserver -dm_ini /dmdata/DAMENG/dm.ini -p DMSERVER执行完会在/etc/systemd/system下生成DmServiceDMSERVER.service。之后就可以用systemctl start DmServiceDMSERVER启动了。注意这个脚本要用 root 执行,或者有 sudo 权限。注册完之后记得systemctl enable DmServiceDMSERVER设置开机自启。
这里有个小经验:如果服务器上跑多个达梦实例,服务名的-p后缀一定要区分开,比如-p BIZ01、-p BIZ02,否则会互相覆盖。还有就是如果后面修改了 dm.ini 里的端口,要重启服务才生效,光 reload 不行。
3. 表空间的规划与创建
3.1 CREATE TABLESPACE语句逐字段拆解
实例起来之后,用 disql 连上去:
./disql SYSDBA/'Dm@Sysdba2024'@localhost:5236然后建业务表空间。我先给一份完整语句,再逐字段解释:
CREATE TABLESPACE "TBS_BIZ" DATAFILE 'TBS_BIZ01.DBF' SIZE 2048 AUTOEXTEND ON NEXT 256 MAXSIZE 20480;DATAFILE后面给的是文件名,如果不带路径,会默认创建在数据目录下(也就是/dmdata/DAMENG/)。建议不带路径,让达梦自己管,这样迁移的时候路径不会出错。如果非要指定绝对路径,注意 DM8 要求该路径必须提前存在,且属于 dmdba 用户。
SIZE单位是 MB,这里给 2048M。关于初始大小怎么定,我的经验是:初始大小给到预期一年数据量的 30% 到 50%,剩下的靠自动扩展。给太小会频繁扩展(每次扩展都涉及文件系统操作,有性能开销),给太大又会浪费存储。
AUTOEXTEND ON NEXT 256表示每次扩展 256M。这个值不要太大会比较安全。MAXSIZE 20480是上限 20G。这里我强烈建议一定要设 MAXSIZE,不要图省事用UNLIMITED。原因很现实:磁盘写满导致数据库挂掉,比表空间满了导致写入失败要严重得多。表空间满了你可以扩容、可以告警、可以处理,磁盘满了整个实例可能直接崩,恢复起来麻烦得多。
索引表空间同理再建一个:
CREATE TABLESPACE "TBS_BIZ_IDX" DATAFILE 'TBS_BIZ_IDX01.DBF' SIZE 1024 AUTOEXTEND ON NEXT 128 MAXSIZE 10240;3.2 扩容的三种做法
表空间满了怎么办?DM8 提供了三种路径,各有各的适用场景。
第一种,给现有的数据文件加自动扩展,或者调大上限:
ALTER TABLESPACE "TBS_BIZ" DATAFILE 'TBS_BIZ01.DBF' AUTOEXTEND ON NEXT 256 MAXSIZE 40960;这种做法最简单,业务无感知,但单个数据文件在部分文件系统上有大小限制(比如 ext4 单文件上限很大,但有些老文件系统有 2G 限制),而且单个文件过大对备份和恢复的时间有影响。
第二种,新增数据文件:
ALTER TABLESPACE "TBS_BIZ" ADD DATAFILE 'TBS_BIZ02.DBF' SIZE 4096 AUTOEXTEND ON NEXT 256 MAXSIZE 40960;这是我最推荐的方式。数据分散在多个文件里,单个文件控制在一定大小内,IO 也能分散到不同文件。而且 DM8 在表空间内的数据文件之间会自动做均衡,新写入的数据会优先往剩余空间多的文件放。
第三种,直接调整已有数据文件的大小:
ALTER TABLESPACE "TBS_BIZ" RESIZE DATAFILE 'TBS_BIZ01.DBF' TO 8192;这个只能调大,不能调小到当前已用空间以下。一般用在预先知道要批量导入大量数据、不想等自动扩展一点点涨的场景。
注意:任何表空间的扩容操作,都要先确认操作系统层面的磁盘剩余空间,别表空间设了 40G 上限,磁盘只剩 5G,那就等着出事。
3.3 巡检SQL和状态管理
日常巡检我常用的几条:
-- 查所有表空间基本信息 SELECT NAME, TYPE$, STATUS$, TOTAL_SIZE, FREE_SIZE FROM V$TABLESPACE; -- 查数据文件和大小 SELECT TABLESPACE_NAME, FILE_NAME, BYTES/1024/1024 AS MB, AUTO_EXTENSIBLE, MAXBYTES/1024/1024 AS MAX_MB FROM DBA_DATA_FILES; -- 按表空间汇总使用率 SELECT TABLESPACE_NAME, SUM(BYTES)/1024/1024 AS TOTAL_MB FROM DBA_DATA_FILES GROUP BY TABLESPACE_NAME;V$TABLESPACE里的TOTAL_SIZE和FREE_SIZE单位大多是页,换算成 MB 要除以 1024 再乘以 PAGE_SIZE/1024,这点第一次看容易算错,我见过有人把页数当成 MB 数,吓了一跳以为表空间快爆了。
表空间还有状态管理。正常情况下是 ONLINE。做维护的时候可以把表空间置为 OFFLINE:
ALTER TABLESPACE "TBS_BIZ" OFFLINE; -- 维护完再拉起来 ALTER TABLESPACE "TBS_BIZ" ONLINE;但要注意,SYSTEM、ROLL、TEMP 这几个系统表空间不能 OFFLINE,只有用户表空间可以。OFFLINE 状态下该表空间里的表无法访问,业务会报错,所以这个操作一般只在实际停机维护窗口里做。
4. 创建用户与模式绑定
4.1 用户、模式、表空间的关系
先把概念理清楚,这三者的关系是很多人混淆的起点。
在 DM8 里,用户是登录身份,模式是对象的命名空间,表空间是物理存储的容器。一个用户默认拥有一个同名模式,用户创建的所有对象(表、视图、索引)默认放在这个模式下。多个用户可以通过授权访问同一个模式下的对象,但一个模式只能属于一个用户(除了系统模式)。
用 MySQL 的思维理解就是:MySQL 的"库"约等于达梦的"模式",MySQL 的"用户"是纯粹的账号和权限集合。所以从 MySQL 迁移时,不要试图把"库"直接对应成"用户",正确的是把"库"对应成"模式",然后单独建一个用户去管它。
还有一个经常被问的问题:CREATE USER和CREATE SCHEMA到底什么关系?答案是,CREATE USER会自动建一个同名模式,你不需要再手动建。如果你确实需要给一个用户建第二个模式,可以用:
CREATE SCHEMA "BIZ_ARCHIVE" AUTHORIZATION "APPUSER";这条语句必须在 SYSDBA 下执行,意思是创建一个属于 APPUSER 用户的模式 BIZ_ARCHIVE。执行完之后 APPUSER 就有了两个模式。这种用法在数据归档场景下比较常见,活跃数据和历史数据用不同模式分开。
4.2 一条完整的建用户语句
CREATE USER "APPUSER" IDENTIFIED BY "App@User2024" DEFAULT TABLESPACE "TBS_BIZ" DEFAULT INDEX TABLESPACE "TBS_BIZ_IDX" ACCOUNT UNLOCK;密码这里有个坑,DM8 默认有一条密码策略,要求密码长度、包含大小写字母、数字、特殊字符等。具体策略由PWD_POLICY参数控制,取值是各位的组合(1 禁止与用户名相同,2 检查密码长度,4 至少包含一个大写字母,8 至少包含一个数字……)。如果建用户时报"密码不符合策略要求",别急着重启改参数,先去查一下:
SELECT * FROM V$DM_INI WHERE PARA_NAME = 'PWD_POLICY';生产环境我不建议把策略关掉,反而应该往上加。密码复杂度是最后一道防线,尤其是数据库这种能直接导出全量数据的系统。如果实在需要临时放宽(比如给某个只读账号设个简单密码做演示),可以调整到最低要求,但演示完记得改回来。
DEFAULT INDEX TABLESPACE这个子句在不同版本的 DM8 上支持情况不太一样,如果提示语法错误,就去掉它,在建索引时显式指定表空间:
CREATE INDEX IDX_ORDER_NO ON "APPUSER"."T_ORDER"("ORDER_NO") TABLESPACE "TBS_BIZ_IDX";4.3 授权到底给到什么程度
建完用户,接下来是授权。DM8 内置了几个角色:
| 角色名 | 权限范围 | 适用场景 |
|---|---|---|
| DBA | 几乎全部权限,不能审计 | 库管理员 |
| RESOURCE | 建表、建视图、建索引、建存储过程 | 业务开发账号 |
| PUBLIC | 基本的查询权限 | 所有用户默认拥有 |
| SOI | 系统视图相关权限 | 运维查询 |
| VTI | 系统动态视图权限 | 监控账号 |
业务账号我一般给 RESOURCE 加 PUBLIC,不给 DBA:
GRANT RESOURCE, PUBLIC TO "APPUSER";为什么不给 DBA?因为 DBA 角色能建用户、能删表空间、能操作其他模式的表。业务程序如果被注入或者有 bug,误删别人数据的风险是实实在在的。权限最小化这个原则,平时感觉不到好处,出事的时候能救命。
如果是给监控平台用的只读账号,那就要更细:
CREATE USER "MONITOR" IDENTIFIED BY "Mon@2024" DEFAULT TABLESPACE "TBS_BIZ"; GRANT PUBLIC TO "MONITOR"; GRANT SELECT ON "APPUSER"."T_ORDER" TO "MONITOR"; GRANT SELECT ON "APPUSER"."T_USER" TO "MONITOR";再配合SELECT ANY TABLE之类的系统权限,具体看监控需要读多少表。还有一种做法是给 VTI 角色,能查动态视图,做性能监控足够用。
4.4 锁定、解锁、改密、删除
这几个操作在运维里出现频率很高,一并说了。
-- 查看用户状态 SELECT USERNAME, ACCOUNT_STATUS, DEFAULT_TABLESPACE, CREATED FROM DBA_USERS; -- 锁定(离职、异常账号) ALTER USER "APPUSER" ACCOUNT LOCK; -- 解锁 ALTER USER "APPUSER" ACCOUNT UNLOCK; -- 改密码 ALTER USER "APPUSER" IDENTIFIED BY "NewPwd@2024"; -- 强制下次登录改密 ALTER USER "APPUSER" PASSWORD EXPIRE; -- 调整表空间配额 ALTER USER "APPUSER" QUOTA UNLIMITED ON "TBS_BIZ"; ALTER USER "APPUSER" QUOTA 5120 ON "TBS_BIZ"; -- 删除用户(谨慎!) DROP USER "APPUSER" CASCADE;QUOTA这个功能值得单独说。它限制的是用户在某个表空间上能使用的空间上限。默认情况下用户建对象是受表空间总大小限制的,不额外限制配额。在多租户共享表空间的场景下,给每个用户设配额能防止一个业务把表空间吃光、其他业务没法写入的情况。
DROP USER ... CASCADE会连同该用户模式下的所有对象一起删掉,这个操作没有回收站,删了就没了。我有个习惯,执行之前先确认没有别的用户在用它的对象:
SELECT GRANTEE, TABLE_NAME FROM DBA_TAB_PRIVS WHERE OWNER = 'APPUSER';再检查有没有外键依赖其他模式的表。这些确认动作加起来不到一分钟,但能避免很多麻烦。
5. 表结构落地:类型、约束与大小写
5.1 字段类型选择和几个容易搞错的点
DM8 的数据类型基本向 Oracle 靠拢,从 MySQL 迁过来的人需要重新建立映射关系。几个关键对照:
| MySQL 类型 | DM8 对应 | 注意点 |
|---|---|---|
| INT | INT / INTEGER | 一致 |
| BIGINT | BIGINT | 一致 |
| TINYINT | TINYINT | 达梦的 TINYINT 有符号范围 -128~127 |
| VARCHAR(n) | VARCHAR(n) | 受 LENGTH_IN_CHAR 影响 |
| TEXT | CLOB / TEXT | TEXT 在 DM8 里底层还是 CLOB |
| DATETIME | DATETIME / TIMESTAMP | DM8 的 DATETIME 精度到毫秒 |
| DECIMAL(m,n) | DECIMAL(m,n) | 一致 |
| BLOB | BLOB | 一致 |
VARCHAR和VARCHAR2在 DM8 里都支持,行为一致,VARCHAR2是从 Oracle 带过来的习惯写法。我建议统一用VARCHAR,语义上更通用。
关于长度,前面强调过LENGTH_IN_CHAR=1时长度单位是字符。但要注意,即使设成 1,字段的字节上限依然存在,比如VARCHAR(8188)在 32K 页大小下的最大长度。一般情况下 VARCHAR 总长不建议超过 4000 字符,太长了管理起来麻烦,大字段应该用 CLOB。
还有一个非常容易踩的坑:DM8 里字符串类型的空串和 NULL 是等价的。INSERT INTO t VALUES('')实际上插入的是 NULL,SELECT出来的也是 NULL。这一点和 Oracle 一致,但和 MySQL 不一致(MySQL 里空串和 NULL 是两回事)。如果业务代码里有用空串判断的逻辑,迁移时要重点检查。
5.2 一份带注释和约束的完整建表脚本
用 APPUSER 登录,建一张订单表作为示例:
CREATE TABLE "APPUSER"."T_ORDER" ( "ID" BIGINT NOT NULL, "ORDER_NO" VARCHAR(32) NOT NULL, "USER_ID" BIGINT NOT NULL, "AMOUNT" DECIMAL(18,2) DEFAULT 0.00 NOT NULL, "STATUS" TINYINT DEFAULT 0 NOT NULL, "REMARK" VARCHAR(500), "CREATE_TIME" TIMESTAMP DEFAULT SYSDATE NOT NULL, "UPDATE_TIME" TIMESTAMP DEFAULT SYSDATE NOT NULL, CONSTRAINT "PK_T_ORDER" PRIMARY KEY ("ID") USING INDEX TABLESPACE "TBS_BIZ_IDX", CONSTRAINT "UK_T_ORDER_NO" UNIQUE ("ORDER_NO") USING INDEX TABLESPACE "TBS_BIZ_IDX" ); COMMENT ON TABLE "APPUSER"."T_ORDER" IS '订单主表'; COMMENT ON COLUMN "APPUSER"."T_ORDER"."ID" IS '主键ID,雪花算法生成'; COMMENT ON COLUMN "APPUSER"."T_ORDER"."ORDER_NO" IS '订单号,业务唯一'; COMMENT ON COLUMN "APPUSER"."T_ORDER"."USER_ID" IS '下单用户ID'; COMMENT ON COLUMN "APPUSER"."T_ORDER"."AMOUNT" IS '订单金额,单位元'; COMMENT ON COLUMN "APPUSER"."T_ORDER"."STATUS" IS '状态:0待支付 1已支付 2已完成 3已取消'; COMMENT ON COLUMN "APPUSER"."T_ORDER"."CREATE_TIME" IS '创建时间'; COMMENT ON COLUMN "APPUSER"."T_ORDER"."UPDATE_TIME" IS '更新时间'; CREATE INDEX "IDX_T_ORDER_USER" ON "APPUSER"."T_ORDER"("USER_ID") TABLESPACE "TBS_BIZ_IDX"; CREATE INDEX "IDX_T_ORDER_CTIME" ON "APPUSER"."T_ORDER"("CREATE_TIME") TABLESPACE "TBS_BIZ_IDX";这里有几个细节值得说明。第一,建表时显式带上模式名"APPUSER"."T_ORDER",即使当前登录用户就是 APPUSER。这样做的目的是让脚本的语义明确,别人看 DDL 一眼就知道表属于谁。第二,主键和唯一约束用USING INDEX TABLESPACE指定索引表空间,这样才真正做到了索引和数据分离。第三,时间类字段给了DEFAULT SYSDATE,但业务系统里我更建议由应用层控制时间,数据库的默认值只作为兜底,原因是应用层的时间更容易统一时区和精度策略。
SYSDATE在 DM8 里返回的是数据库服务器时间,精度到秒。需要毫秒精度的用SYSTIMESTAMP。
5.3 索引表空间分离与主键选择
主键用什么类型,这个在达梦上讨论得比 MySQL 少,但同样重要。自增主键在 DM8 里支持IDENTITY(1,1):
"ID" BIGINT IDENTITY(1,1) NOT NULL这个写法简单,但会带来一个问题:插入时必须让数据库生成主键,应用层拿不到预生成的值(除非用SELECT @@IDENTITY之类的函数)。在分库分表或者需要提前知道主键的场景下,我倾向用应用层生成(比如雪花算法),字段就写成普通 BIGINT。
关于索引,DM8 的索引类型主要是 B 树索引,默认就是 B 树。还有位图索引(BITMAP)适合低基数列,比如状态字段,但位图索引在并发写入时会锁住整个位图段,OLTP 场景慎用。函数索引在 DM8 里也支持:
CREATE INDEX "IDX_T_ORDER_MONTH" ON "APPUSER"."T_ORDER"(SUBSTR("ORDER_NO", 1, 6)) TABLESPACE "TBS_BIZ_IDX";顺带提一句热词里出现的"生成首拼码函数"。达梦没有内置的拼音首字母转换函数,需要自己写。常见的做法是建一张汉字到首字母的映射表,然后写一个 PL/SQL 函数逐字查表拼接。数据量不大的话,也可以把映射关系直接写进函数的 CASE 分支里。这种函数在客户名称、商品名称的模糊检索场景下很有用,比如输入"zlyy"能匹配到"中联医药"。要注意的是,映射表方案在字符集为 GBK 时需要按字节处理,UTF-8 下按字符处理,两者的实现逻辑完全不同,写之前先确认库的字符集。
6. 客户端接入:disql、管理工具和第三方工具
6.1 disql与DM管理工具
原生工具是最省事的。disql 是命令行客户端,在/opt/dmdbms/bin下:
./disql APPUSER/'App@User2024'@localhost:5236登录进去之后,\l列出所有模式,\d描述当前模式的表,\c切换连接。脚本执行用:
./disql APPUSER/'App@User2024'@localhost:5236 -f /tmp/init.sqlDM管理工具(Manager)是图形化的,类似 PL/SQL Developer 的定位,功能挺全,建表、执行 SQL、看执行计划、导出 DDL 都能做。这个工具在安装目录的/opt/dmdbms/tool下,需要在有图形界面的环境下运行,远程用的话得配 X11 转发或者 VNC。现在达梦也出了 Web 版的工具,部署在服务端,浏览器直接访问,比 X11 转发方便。
6.2 第三方客户端的配置要点与报错排查
很多人习惯用 Navicat 或者 DataGrip 连达梦,这块有几个容易卡住的点。
用 Navicat 连接 DM8,需要选对连接类型。新版 Navicat Premium 已经内置了达梦的驱动选项,选"达梦"或者"DM"即可,端口默认 5236,主机填服务器地址。如果 Navicat 版本比较老,找不到达梦连接类型,那就比较麻烦了,因为它不支持像 MySQL 那样自己加载任意 JDBC 驱动。
用 DataGrip 或者 DBeaver 这类支持自定义驱动的工具,配置就灵活多了。驱动类名dm.jdbc.driver.DmDriver,JDBC URL 格式:
jdbc:dm://192.168.1.100:5236 jdbc:dm://192.168.1.100:5236/APPUSER第二个写法带了模式名,连接后默认在 APPUSER 模式下。需要指定字符集的话加参数:
jdbc:dm://192.168.1.100:5236?characterEncoding=UTF-8驱动 JAR 包在/opt/dmdbms/drivers/jdbc/DmJdbcDriver18.jar,注意要选和 JDK 版本匹配的,JDK 8 用DmJdbcDriver18.jar这个名字有点误导,看名字是 18 但实际支持 JDK 1.8,具体版本对应关系看驱动目录下的说明文件。
常见的连接失败原因,我按出现频率排一下:
| 报错信息 | 大概率原因 | 处理方式 |
|---|---|---|
| 连接超时 | 防火墙没开 5236 端口 | 检查 firewalld 或 iptables |
| 用户名或密码错误 | 大小写、密码策略 | 用 disql 本地验证一次 |
| 网络通信异常 | 客户端和服务端版本不匹配 | 换用和服务端同版本的驱动 |
| 中文乱码 | JDBC URL 少了字符集参数 | 加上 characterEncoding=UTF-8 |
| 找不到驱动类 | JAR 包没加载或版本不对 | 重新指定驱动文件路径 |
顺带说说 Nacos 适配达梦的场景,这两年问的人挺多。Nacos 从 2.2 版本开始支持多数据源,要让它用达梦做配置存储,需要把达梦的 JDBC 驱动包放进 Nacos 的 plugins 目录,然后在 application.properties 里改数据源配置,把spring.datasource.platform设成dm,并配置对应的驱动类名和 URL。坑在于 Nacos 的建表脚本默认只有 MySQL 版本,达梦需要自己改 DDL,注意把AUTO_INCREMENT换成IDENTITY,ENGINE=InnoDB这些 MySQL 专有语法全部删掉。
7. 导入导出、备份与几个上线后才暴露的坑
7.1 dexp/dimp的编码参数
这是我在达梦上踩得最深的坑,没有之一。用 dexp 导出的 dmp 文件,换一台服务器用 dimp 导入,中文全是乱码或者直接报编码错误。
事情的本质是这样的:dexp/dimp 的ENCODING参数控制的是 dmp 文件本身的编码,它和数据库实例的字符集是两个独立的东西。默认情况下,导出的 dmp 文件编码跟随数据库字符集。但如果导出和导入的两端字符集不一致,就必须显式指定。
导出时:
./dexp USERID=APPUSER/'App@User2024'@localhost:5236 \ FILE=/tmp/app.dmp \ LOG=/tmp/app_exp.log \ SCHEMAS=APPUSER \ ENCODING=UTF-8导入时:
./dimp USERID=APPUSER/'App@User2024'@localhost:5236 \ FILE=/tmp/app.dmp \ LOG=/tmp/app_imp.log \ ENCODING=UTF-8热词里提到的PG_GBK和PG_UTF8是编码参数的另一种写法(带 PG_ 前缀),两种写法达梦都认。遇到"导入时本地编码:PG_GBK, 导入文件编码:PG_UTF8"这种提示,说明 dmp 里记录的文件编码和当前实例的本地编码不匹配。
处理办法分两种情况。如果只是编码标记不一致但内容没问题,加参数忽略检查:
./dimp ... ENCODING=UTF-8 IGNORE_INIT_ERROR=1或者用NOLOG之类的参数先试导。但如果内容真的乱码了,那就得先把源库的数据以正确编码重新导出一次。我的做法是导出前先确认两边字符集:
SELECT UNICODE();返回 0 是 GB18030,1 是 UTF-8。两侧不一致的话,在导出时就把 dmp 转成目标库的编码,而不是导入时硬转,成功的概率高很多。
还有一点,跨平台迁移时注意文件名大小写和路径分隔符。Linux 和 Windows 的数据文件路径写法不同,如果 dmp 里记录了绝对路径,导入到不同目录结构的机器上会失败,用REMAP_SCHEMA和REMAP_TABLESPACE参数重映射:
./dimp ... REMAP_SCHEMA=OLD_USER:NEW_USER REMAP_TABLESPACE=TBS_OLD:TBS_NEW7.2 物理备份和逻辑备份的分工
达梦的备份分两套体系。
物理备份用 dmrman,备份的是数据文件、控制文件的二进制副本,速度最快,适合全库级别的完整保护。执行前需要确保 DmAPService 服务是启动的(这是达梦的辅助进程服务,负责备份相关的底层操作),否则会报错。一个基础的完全备份:
./dmrman CTLSTMT="BACKUP DATABASE '/dmdata/DAMENG/dm.ini' FULL TO BACKUP_FILE1 BACKUPSET '/dmbackup/full_20240101'"物理备份的恢复要在停机状态下做,恢复过程涉及RESTORE和RECOVER两个阶段,还需要配合归档日志才能恢复到最新状态。所以生产环境一定要开归档:
ALTER DATABASE MOUNT; ALTER DATABASE ARCHIVELOG; ALTER DATABASE ADD ARCHIVELOG 'DEST=/dmarch, TYPE=LOCAL, FILE_SIZE=1024, SPACE_LIMIT=102400'; ALTER DATABASE OPEN;逻辑备份就是我前面说的 dexp/dimp,导出的是 SQL 层面的对象和数据。它的优点是跨版本、跨平台兼容性好,能选择性导出(按模式、按表),缺点是慢,大数据量下不太实用。我的实践是:日常全量用物理备份,版本升级、数据迁移、按表恢复用逻辑备份,两者配合,别指望一个能覆盖所有场景。
7.3 实测踩过的坑
最后说几个实际部署中遇到、但文档里不太提的问题。
第一个,表空间创建之后数据文件没有立刻落盘。这是正常的,达梦用的是稀疏文件(thin provisioning),SIZE 给了 2G 不代表磁盘上立刻占用 2G。用du -sh看和ls -l看的大小会差很多,别以为文件没建成功。想确认实际占用,du -sh更准。
第二个,ALTER USER ... QUOTA对已有数据不生效校验。也就是说,如果你先建了 5G 数据,再设配额 2G,这条语句不会报错,但后续写入会被拒绝。配额检查只在写入时做,不做历史数据的回溯校验,这点要清楚。
第三个,索引表空间没设好,重建索引时会报错。特别是用了USING INDEX TABLESPACE的约束索引,如果那个表空间被 OFFLINE 了,对表的写入会直接失败,报"索引不可用"。所以索引表空间的可用性直接关系到业务写入,监控不能漏。
第四个,LENGTH_IN_CHAR与现有数据迁移的冲突。从 MySQL 或者 Oracle 迁过来的表,如果原库的 VARCHAR 长度是按字节算的,迁到达梦后长度语义可能变了。我在一个项目里遇到过,原库VARCHAR(60)存了 20 个汉字(UTF-8 下 60 字节),迁到达梦后 LENGTH_IN_CHAR=1,60 变成了 60 个字符,看起来更宽松了没问题;但如果反过来,达梦的VARCHAR(60)迁移到字节语义的库,就会截断。迁移前一定要做长度语义的对照测试,挑几个存了最长内容的字段实际跑一遍。
第五个,用户删除时的依赖检查。前面提过要查DBA_TAB_PRIVS,但还有一个更隐蔽的:视图、存储过程、触发器里可能引用了这个用户的对象。这些依赖藏在DBA_DEPENDENCIES里:
SELECT OWNER, NAME, TYPE, REFERENCED_OWNER, REFERENCED_NAME FROM DBA_DEPENDENCIES WHERE REFERENCED_OWNER = 'APPUSER';我有个习惯,删用户之前先把这条查询跑一遍,把结果贴到变更单里作为附件。这样做一方面是为了安全,另一方面是万一真出问题了,事后能回溯到底删了什么。
我个人在实际操作中的体会是,达梦 DM8 的这套体系其实并不复杂,用户、模式、表空间三个概念理清之后,剩下的都是熟练度问题。真正容易出事的不是语法写错,而是规划缺失——表空间不设上限、字符集初始化时随手指一个、权限图省事全给 DBA。这些决定在项目初期花不了半小时,但能省掉后期几天的返工。所以每次新建环境,我都会先花点时间把这几张表列出来:实例参数怎么定、分几个表空间、每个多大、上限多少、几个用户、各给什么权限。列清楚了再动手敲命令,比边建边想靠谱得多。