手头一台纯 Linux 服务器,客户甩过来一个十几年的 .mdb 老库,说里面是历史业务数据,要完整搬进 SQL Server。Windows 没有,Access 也装不了,更不可能为了这一次迁移专门配一台 Windows 机器。放弃又不甘心,因为数据量不大,纯粹是格式和工具链的问题。这种场景我遇到不止一次,最后固定下来一套流程:用 Docker 起一个 SQL Server 容器,直接在容器内装好读取 Access 数据的工具链,再把 .mdb 里的表结构和数据导进去。这篇文章就是把这条路径从头到尾拆开讲清楚,包括为什么这么选、每步参数怎么定、以及最容易翻车的几个坑。
如果你也在 Linux 上碰过 .mdb 文件,或者马上要面对一次 Access 老库迁移,这篇文章可以照着抄。整个过程只依赖 Docker 和一个老牌开源工具 mdbtools,不需要 Windows,不需要图形界面,也不需要昂贵的迁移软件。
1. 先想清楚整体方案:为什么选择 Docker 加容器内工具链
1.1 三个常见导入路径的取舍
在决定具体做法之前,我把能用上的路径都过了一遍。最容易想到的是在 Windows 机器上用官方迁移助手,把 Access 库直接转成 SQL Server 库,这条路径最平滑,但前提是你有一台能用的 Windows。服务器在 Linux 机房,临时去搞 Windows 授权和图形会话,成本远超预期。
第二条路径是 Docker 部署 SQL Server,数据文件放到宿主机,然后在宿主机上装一套 mdbtools 和数据库客户端来做导入。这条路可行,但工具链东一块西一块:宿主机要装 ODBC 驱动,要装 mdb 工具,还要处理 Python 环境和权限,出了问题排查链路很长。
第三条路径,也就是我现在固定用的方式:让 SQL Server 容器同时承担工具链的职责。官方 SQL Server 容器底层是一个完整的 Ubuntu 系统,可以在里面用 apt 安装 mdbtools 等工具。这样整个导入过程全部发生在容器内部,文件路径干净,依赖关系清晰,宿主机只需要装一个 Docker。实际操作下来,这种方式最稳,也最容易被复现。
1.2 为什么不能指望 SQL Server 自己读 .mdb
要理解这个方案,得先搞清楚一个关键事实:Linux 版的 SQL Server 不具备直接读取 .mdb 文件的能力。你在 Windows 版 SQL Server 里可能用过 OpenRowSet 配合 Access 的 OLEDB 驱动去查 mdb,这套机制依赖微软在 Windows 上提供的 ACE 或 Jet 引擎组件,而 Linux 容器里根本没有这些组件,也不可能在容器里装 Windows 驱动。
所以现实一点说,.mdb 只是一个被动的数据源,需要有一个第三方工具把它解析出来,转换成通用的 CSV 或 SQL 语句,再交给 SQL Server 消化。mdbtools 就是这个第三方角色。
mdbtools 是一套开源工具,专门用来读取 Access 数据库文件。它包含几条核心命令:mdb-tables列出所有表,mdb-schema输出表结构,mdb-export把表数据导出为文本。它不依赖图形界面,不依赖 Access 运行时,而且对 .mdb 格式的兼容性相当不错,这正好满足 Linux 环境下的需求。
选择在 SQL Server 容器内安装这套工具,还有一个额外的好处:导出的 CSV 文件可以直接写到容器磁盘上,然后立刻用 T-SQL 的BULK INSERT语句导入,文件路径不用跨容器传递,也省去了 docker cp 的步骤。
2. 部署 SQL Server 容器时的参数细节
2.1 镜像选择和环境变量的讲究
镜像用官方最新的稳定版本,拉取命令如下:
docker pull mcr.microsoft.com/mssql/server:2022-latest启动容器的完整命令是这个样子的:
docker run -d \ --name mssql \ -e 'ACCEPT_EULA=Y' \ -e 'MSSQL_SA_PASSWORD=Str0ng!Passw0rd' \ -e 'MSSQL_PID=Developer' \ -p 1433:1433 \ -v sqlserver_data:/var/opt/mssql \ mcr.microsoft.com/mssql/server:2022-latest逐个解释一下这些参数,因为每一个都藏着坑。
ACCEPT_EULA=Y是必须的,不接受许可协议容器直接退出。MSSQL_SA_PASSWORD是 sa 用户的密码,这里要特别强调,密码必须满足 SQL Server 的密码复杂度要求:至少 8 位,同时包含大写字母、小写字母、数字和符号。如果密码太简单,比如只设了123456,容器看起来是启动了,但会反复重启,去查日志才会看到密码策略校验失败的错误。这个坑我踩过不止一次。
MSSQL_PID=Developer指定版本。Developer 版是免费的开发版,功能上和企业版完全一致,只是不允许用于生产环境。对于这种导入迁移的临时任务,用 Developer 版完全没问题。默认值其实是 Evaluation 版,只能用 180 天,到期后实例会停止,所以最好显式指定。
端口映射-p 1433:1433把宿主机的 1433 端口转发到容器内的 1433。如果你宿主机已有其他服务占用 1433,或者想用非标准端口,可以在左边改,比如-p 14330:1433,客户端连接时用 14330 即可。
2.2 数据持久化和资源占用
-v sqlserver_data:/var/opt/mssql这一条很多人会忽略,但实际非常重要。SQL Server 的数据文件默认写在容器的/var/opt/mssql目录,如果不做数据卷映射,容器一旦被删除,整个数据库就永久丢了。做迁移任务的周期往往比较长,中间可能多次删容器重建,有了命名数据卷,数据库文件就能保留下来。
官方镜像对宿主机的资源要求是至少 2GB 内存。这个不是随口说的,是 SQL Server 引擎本身的硬性需求,内存不够的机器上实例可能启动失败或者运行异常。磁盘方面,镜像本身体积不小,解压后占用好几个 GB,再加上数据库文件和后续的 CSV 文件,建议预留 20GB 以上的可用空间。
容器启动后,可以通过健康检查来判断实例是否真正就绪。最简单的方式是进入容器尝试连接:
docker exec -it mssql bash cat /var/opt/mssql/log/errorlog | grep -i "ready for client connections"看到类似 "SQL Server is now ready for client connections" 的日志,就说明实例已经可用了。
2.3 容器内 SQL 工具的安装补充说明
这个版本的 SQL Server 容器内默认并不像旧版本那样自带 sqlcmd 工具。如果需要先在容器里执行建库建表语句,可以自行安装微软官方的命令行工具。在容器内执行:
apt-get update apt-get install -y curl gnupg2 curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list | tee /etc/apt/sources.list.d/mssql-release.list apt-get update ACCEPT_EULA=Y apt-get install -y mssql-tools18 unixodbc-dev安装完成后,sqlcmd 的路径在/opt/mssql-tools18/bin/sqlcmd。注意这个 18 版本的 sqlcmd 默认要求加密连接,所以连接本地实例时通常要加-C参数来信任服务器证书:
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Str0ng!Passw0rd' -C -Q "SELECT @@VERSION"能正确输出版本号,说明数据库服务正常,接下来就可以做数据导入的准备工作了。
3. 在容器内构建 .mdb 读取工具链
3.1 安装 mdbtools 及基础验证
在同一个容器内继续操作,把 mdbtools 装上去:
apt-get install -y mdbtools安装完成后,先用工具探一下这个 .mdb 文件的基本情况。假设数据文件叫legacy.mdb,放在了/var/opt/mssql/files/目录下(这个目录属于数据卷,后面会用到):
mdb-ver /var/opt/mssql/files/legacy.mdbmdb-ver会输出 Access 文件的版本信息,比如 Jet 4.0 或者 Access 2007 之类的标识,确认文件能被正常识别。然后列出所有表:
mdb-tables -1 /var/opt/mssql/files/legacy.mdb-1参数表示每行只输出一个表名,方便脚本处理。如果这一步报错或者输出为空,先不要继续,优先排查文件本身是不是完整、是不是有密码保护。
3.2 侦查表结构:字段类型映射是重中之重
表名拿到后,下一步是看结构。用mdb-schema输出建表语句:
mdb-schema -T 客户表 --sql-server /var/opt/mssql/files/legacy.mdb注意--sql-server这个参数,它会让 mdbtools 输出符合 SQL Server 语法的创建表语句,而不是默认的 Access 语法。输出大概长这样:
CREATE TABLE [客户表] ( [ID] INTEGER NOT NULL, [客户名称] NVARCHAR(255), [联系人] NVARCHAR(255), [创建时间] DATETIME, [是否启用] BIT, ... );看结构的时候要多留心类型映射问题。Access 里习惯用 Text 表示短文本,Memo 表示长文本,时间是 DateTime,Yes/No 是布尔,自动编号是 AutoNumber。mdbtools 转换成 SQL Server 语法时,Text 一般映射成 NVARCHAR(255),Memo 映射成 NTEXT 或 NVARCHAR(MAX),DateTime 映射成 DATETIME,这个方向是对的。
但这里有一个经验之谈:生成的 DDL 通常只能作为起点。NVARCHAR(255) 对中文场景可能偏短,Memo 字段如果实际存放大量文本,建议改成 NVARCHAR(MAX)。日期字段在 Access 里允许为空的,导入后 SQL Server 也是可空,这个不用动。
3.3 mdbtools 的边界和限制
在使用 mdbtools 之前,要清楚它的能力边界,否则会在某个环节卡住很久。
第一个限制是文件格式。mdbtools 对 .mdb 格式支持很成熟,但 Access 2007 之后流行的 .accdb 格式,mdbtools 的支持非常有限,很多情况下列表都读不出来。如果你手里只有一个 .accdb 文件,需要先用 Windows 版 Access 把它另存为 .mdb 格式,或者直接转成 CSV,再走后面的流程。
第二个限制是密码保护。如果 .mdb 文件设置了数据库级密码,mdbtools 无法解密读取,执行mdb-tables时会报错或者列出空表。这种情况没有绕过办法,必须找有 Access 环境的人先清除密码。用户级权限这种复杂情况也同理,不要试图用命令去破解,老老实实先去掉访问限制。
第三个限制是它只是读取工具,不会保留 Access 里的窗体、报表、宏和 VBA 代码。如果你的迁移目标包含这些应用层的对象,mdbtools 帮不了你,需要另外考虑迁移策略。绝大多数业务迁移只关心表和里面的数据,这一点通常可以接受。
4. 建库、建表与数据的完整导入流程
4.1 创建目标数据库
进入 sqlcmd,创建本次迁移专用的库,顺便规划好数据文件的目录:
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Str0ng!Passw0rd' -C -Q "CREATE DATABASE MigrationDB"建议新建一个独立数据库来做迁移,不要直接塞进 master 或某个现有业务库。原因很简单:导入过程难免出错,独立库方便反复重建,验证完成后需要上线时再处理,不会影响其他数据。
切换到目标库继续操作:
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Str0ng!Passw0rd' -C -d MigrationDB4.2 根据 mdb-schema 生成建表语句并修正
把之前mdb-schema的输出拿到手,人工审查一遍,修正几个典型问题,然后逐条执行。
常见修正场景包括:把 NTEXT 统一改成 NVARCHAR(MAX),把 NVARCHAR(255) 按实际数据长度适当扩位,为每个表补充主键约束,给经常按日期查询的字段加上索引。业务字段的中文注释,如果 Access 原表没有,也可以在导入前补上。
执行建表语句时可以写成一个.sql文件批量执行。把文件放到/var/opt/mssql/files/schema.sql,然后在容器内执行:
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Str0ng!Passw0rd' -C -d MigrationDB -i /var/opt/mssql/files/schema.sql4.3 用 mdb-export 导出数据为 CSV
建表完成之后,开始导数据。先用一张小表验证整个链路,我这里假设有一张只有几十行的配置表:
mdb-export \ -b ';' \ -D '%Y-%m-%d %H:%M:%S' \ -H \ /var/opt/mssql/files/legacy.mdb \ 配置表 \ > /var/opt/mssql/files/配置表.csv这里参数需要解释一下。-b ';'指定字段分隔符为分号而不是默认的制表符或逗号。为什么不用逗号?因为业务数据里出现逗号的概率太高了,导出后再用逗号做分隔符几乎一定会出现字段错位。分号在中文业务场景里相对少见。-D '%Y-%m-%d %H:%M:%S'强制日期字段输出为 ISO 风格格式,避免 Access 默认的本地化日期格式给后续导入带来解析问题。-H让第一行输出列名,这样导出的 CSV 有表头,后面 BULK INSERT 时可以直接指定跳过首行。
导出前先确认表里有多少行数据,导出的文件行数减去表头行数应该对上。可以用:
wc -l /var/opt/mssql/files/配置表.csv再看一眼文件内容的片段,确认没有乱码、没有奇怪的换行符:
head -n 5 /var/opt/mssql/files/配置表.csv4.4 用 BULK INSERT 把 CSV 导入 SQL Server
数据文件在容器本地,路径清晰,接下来用BULK INSERT一步到位。在 sqlcmd 里执行:
BULK INSERT [MigrationDB].[dbo].[配置表] FROM '/var/opt/mssql/files/配置表.csv' WITH ( FORMAT = 'CSV', FIRSTROW = 2, FIELDTERMINATOR = ';', ROWTERMINATOR = '\n', CODEPAGE = '65001', TABLOCK );逐项解释这些参数的意义。FORMAT='CSV'告诉 SQL Server 按 CSV 语义解析,允许处理引号包裹的字段。FIRSTROW=2是因为第一行是列名,要跳过。FIELDTERMINATOR=';'必须和 mdb-export 导出时的分隔符保持一致。ROWTERMINATOR='\n'默认行终止符是\n,如果文件是在 Windows 下生成的,要改成\r\n,但 mdbtools 在 Linux 下导出,所以\n就够了。CODEPAGE='65001'声明文件是 UTF-8 编码,这也是 mdbtools 的默认输出编码,这个参数不加的话,中文内容很可能变成乱码。
如果原表有自增主键且需要保留原来的 ID 值,加上KEEPIDENTITY选项。没有这个参数时,BULK INSERT 会把自增列的值忽略掉,重新从 1 开始生成,历史数据的 ID 就保不住了。
执行成功后 sqlcmd 会返回受影响的行数。逐表操作,把 .mdb 里的所有表都走一遍这个流程。
4.5 全库批量导出的脚本化思路
表数量一多,手工执行效率就很低。可以把表和顺序整理成一个清单,写一个简单的 shell 循环完成导出和导入。比如先手动获取全部表名存入数组,然后循环执行:
tables=$(mdb-tables -1 /var/opt/mssql/files/legacy.mdb) for table in $tables; do echo "导出表: ${table}" mdb-export -b ';' -D '%Y-%m-%d %H:%M:%S' -H \ /var/opt/mssql/files/legacy.mdb "${table}" \ > "/var/opt/mssql/files/${table}.csv" done导入部分可以用 sqlcmd 逐表执行 BULK INSERT。写脚本的思路很简单,但有几个细节要注意:表名里如果包含空格或中文,循环变量要加引号;生成的 CSV 文件名不要和现有路径冲突;中途某张表失败时,记录日志而不是中断整个循环,方便最后统一排查。
我个人习惯在批量执行前,先用一张小表验证分隔符、编码和日期格式全部正确,再放开跑。一次跑全量,出来的错误数量很难一次消化。
5. 避坑清单:常见问题与排查实录
5.1 连接与容器启动类问题
容器反复重启。最典型的场景是 sa 密码不符合复杂度要求,或者ACCEPT_EULA没设置。排查方式是看容器日志:
docker logs mssql | tail -n 30日志里会直接给出错误原因,按提示修正环境变量重建容器即可。
sqlcmd 连接报 SSL 证书错误。新版 mssql-tools18 默认强制加密连接,自签名证书会导致校验失败。解决方式是加-C参数信任证书。这个报错很容易让人误以为密码不对,其实是连接方式不对。
宿主机连接不到容器实例。先确认端口映射有没有设置,使用docker ps查看映射关系;再检查宿主机的防火墙是否放行 1433 端口;最后确认容器内实例是否正常监听。有时候容器起来了,但 SQL Server 引擎还没有完全就绪,连接被拒绝,等几秒再试就好。
5.2 数据导入类问题
导入后中文字符全是问号或乱码。绝大多数情况是CODEPAGE设置不对。mdbtools 导出的是 UTF-8,BULK INSERT 必须声明CODEPAGE='65001'。另外检查目标表字段类型是否为 NVARCHAR 系列,如果用 VARCHAR 存储中文,也会在更底层的环节出现截断或乱码。
日期字段解析失败。mdb-export 如果不指定-D,输出格式可能是2023-04-15 16:30:00或者带 AM/PM 的本地化格式,BULK INSERT 不一定能正确识别。导出时统一用-D '%Y-%m-%d %H:%M:%S'生成标准格式,同时目标列为 DATETIME2 或 DATETIME,解析就非常顺畅。
NULL 被导入成空字符串。Access 的空值和空字符串是两个概念,但导出为 CSV 后可能都显示为空。导入后,如果业务逻辑强依赖空值判断,需要在导入后执行一次字段清理,把空字符串统一更新为 NULL,或者反过来按业务需要处理。
自增主键冲突。导入历史数据时如果没有用KEEPIDENTITY,会出现主键重新编号的问题。如果原表主键被其他表引用,而且关联关系基于这个主键,那导入后数据就错乱了。所以带自增主键的表,一律显式加KEEPIDENTITY。
5.3 批量导入的性能和稳定性
小表导入毫无压力,但遇到百万行级别的历史流水表,还是要注意几个点。
导入前先不要急着建大量索引,尤其是非聚集索引。数据插入过程中每一条记录都会触发索引维护,速度会明显下降。正确的做法是:先建表,只保留主键约束,然后导入数据,等数据全部落地后再统一创建业务查询需要的索引。这与平时的建表习惯相反,但批量导入场景下效率差别非常大。
事务日志方面,如果数据库是完整恢复模式,百万行级别的导入会产生大量日志,可能把磁盘撑爆。迁移用的临时库可以考虑在导入期间把恢复模式切换为简单模式,导入完成后再改回来。这个操作要在导入前完成,否则已经膨胀的日志文件不会自动收缩。
大文件导入还可以用BATCHSIZE参数控制单批提交的行数,避免巨量数据一次性写入导致内存和日志压力。比如:
BULK INSERT [MigrationDB].[dbo].[流水表] FROM '/var/opt/mssql/files/流水表.csv' WITH ( FORMAT = 'CSV', FIRSTROW = 2, FIELDTERMINATOR = ';', ROWTERMINATOR = '\n', CODEPAGE = '65001', BATCHSIZE = 10000, TABLOCK );TABLOCK让导入过程获取表级锁,减少锁开销,也能明显提升导入速度。代价是导入期间这张表不可并发访问,迁移场景完全可以接受。
5.4 文件放在哪里才算安全
最后才提这个,是因为它往往是最隐蔽的问题。mdb 文件、导出的 CSV、schema 脚本,这些文件如果直接放在容器可写层,容器一旦删除,这些工作成果全部丢失。正确做法是放在挂载卷对应的目录里,也就是本文使用/var/opt/mssql/files/这个路径的原因——它属于数据卷,容器重建后文件仍然在。
docker cp 可以把文件从宿主机塞进容器,但要记住,如果目标路径不在数据卷里,容器删除后文件还是会消失。
我在实际操作中最深的体会是:这类 .mdb 迁移项目,表面上难在工具链和命令参数,实际上真正花时间的地方全在数据本身。字段类型不对、空值语义不一致、主键关联被打破,这些问题只有在投入实测之后才会暴露。磨刀不误砍柴工,先把 .mdb 里的表结构和数据特征完整摸一遍,再进入正式的建表和导入环节,整个过程会顺很多。至少我自己现在做这类任务,第一步永远是 mdb-tables 和 mdb-schema 两个命令先把全貌看清,后面基本不会再碰到让人头疼的返工。