手头一台纯 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_EULAY \ -e MSSQL_SA_PASSWORDStr0ng!Passw0rd \ -e MSSQL_PIDDeveloper \ -p 1433:1433 \ -v sqlserver_data:/var/opt/mssql \ mcr.microsoft.com/mssql/server:2022-latest逐个解释一下这些参数因为每一个都藏着坑。ACCEPT_EULAY是必须的不接受许可协议容器直接退出。MSSQL_SA_PASSWORD是 sa 用户的密码这里要特别强调密码必须满足 SQL Server 的密码复杂度要求至少 8 位同时包含大写字母、小写字母、数字和符号。如果密码太简单比如只设了123456容器看起来是启动了但会反复重启去查日志才会看到密码策略校验失败的错误。这个坑我踩过不止一次。MSSQL_PIDDeveloper指定版本。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_EULAY 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 表示长文本时间是 DateTimeYes/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 );逐项解释这些参数的意义。FORMATCSV告诉 SQL Server 按 CSV 语义解析允许处理引号包裹的字段。FIRSTROW2是因为第一行是列名要跳过。FIELDTERMINATOR;必须和 mdb-export 导出时的分隔符保持一致。ROWTERMINATOR\n默认行终止符是\n如果文件是在 Windows 下生成的要改成\r\n但 mdbtools 在 Linux 下导出所以\n就够了。CODEPAGE65001声明文件是 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-8BULK INSERT 必须声明CODEPAGE65001。另外检查目标表字段类型是否为 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 两个命令先把全貌看清后面基本不会再碰到让人头疼的返工。