
简介InterBase 6.0 数据库安装包与开发示例合集面向使用 Delphi 6 构建客户端/服务器数据库应用的开发者及数据库学习者可用于搭建本地测试环境、研究早期 RDBMS 的部署结构与 API 调用方式。压缩包共 130 个文件约 4.86MB含 33 个 C 源码、15 个可执行程序、8 个动态库以及头文件、库文件、帮助文档、SQL 脚本和 gdb、gbk 示例数据库覆盖安装、卸载、运行配置与基础数据操作。已有 341 人学习下载对遗留系统维护和旧版 Delphi 数据访问研究仍有参考价值。读者可获得较完整的安装组件、数据库引擎依赖文件、编程接口示例和初始数据库样本便于理解 BDE、ODBC 等连接方式背后的文件组成也可用于排查旧项目运行环境缺失、数据库初始化与许可配置等问题。1. 接手一个只剩 .GDB 文件的老系统才明白 InterBase 6.0 是什么形态交付包里没有安装手册只有一个 4MB 出头的.GDB文件和一堆 Delphi 控件源码这是不少人第一次接触 InterBase 6.0 时的真实场景。它并不是那种需要独立 DBA 运维的数据库服务整个库就是一个文件拷到目标机器、配好连接串、把控件拖到窗体上就能读写。这份资源的价值也在这里服务端、客户端连接库、控件源码和示例应用凑成一套拿下来就能把一个数据库应用从前端到存储层全部跑通。这篇按我拆包的顺序写先讲清 InterBase 6.0 的事务模型和文件组织再把它在本机跑起来然后把生成器、触发器、存储过程、UDF 这几块核心实现逐段拆开最后落在「拿到控件源码之后改哪里」这个实际问题上。手里攥着老项目要维护的人和想拿一个轻量嵌入式数据库练手的人都能照着走一遍。2. 技术底座MGA 架构下的事务版本与页面组织2.1 多代版本架构读不挡写写不挡读先纠正一个很容易带偏的认知InterBase 6.0 不是靠回滚段做并发的。执行 UPDATE 时它不原地覆盖那一行而是把记录的新版本写进数据页旧版本留在原处只在记录头里把「替换事务号」标成当前事务。别的读事务只要自己的快照事务号比这个替换号小就继续读到旧版本一点都不会被卡住。这就是所谓多代版本架构MGA它的直接结果是读事务永远不用等写事务写事务也不会因为有人在扫表而排队。没有回滚段带来的另一个差别在回滚语义上。执行 ROLLBACK 时数据库并不做「反向补偿」它只是把那个事务在事务清单里标记为已死这个事务写下的所有版本立刻变成垃圾数据等着被回收。所以回滚一个刚插了十万行的事务几乎瞬间完成不会像日志型数据库那样还要回放一遍。这套设计的代价也很明确垃圾版本不会自动消失需要靠 sweep 机制回收而 sweep 能不能推进取决于「最老活跃事务」OIT和「最老事务」OAT这两个水位线能不能往前走。这就引出了老项目里最典型的一类故障。只要有一个连接开着事务不提交——哪怕它只是SELECT了一下——OAT 就被钉在原地比它新的所有垃圾版本都回收不了库文件只涨不缩查询越来越慢。我在一个工控项目上见过库文件从 200MB 涨到 3GB最后查出来是一个报表模块的TIBTransaction忘了Commit挂了半个月。所以看一个 InterBase 库健不健康第一件事不是看数据量是看 OIT 和 OAT 差了多远。还有一点要注意事务隔离级别在 InterBase 里是通过 TPB事务参数块传的isc_tpb_concurrency是快照隔离最常用isc_tpb_read_committed配isc_tpb_rec_version是读已提交而isc_tpb_consistency会加表级锁除非你在做批处理维护否则别碰它一旦用错整个系统的并发直接降到单线程。2.2 单文件、页与事务计数器的物理组成.GDB文件内部是按页组织的页大小在建库时定死可选 1024、2048、4096、8192 字节。第 0 页是数据库头记录了页大小、事务水位、影子文件信息往后是页分配页、事务清单页TIP、指针页、数据页、索引页和 BLOB 页。理解这套分层最实用的工具是gstat它不做任何修改只读取元信息。# 只看库头页大小、OAT/OIT、下一个事务号、影子文件 gstat -h /opt/data/demo.gdb # 看数据页的填充率和高水位判断是不是碎片太多 gstat -d /opt/data/demo.gdb | more # 看索引深度深度从 3 变成 4 通常意味着索引膨胀 gstat -i /opt/data/demo.gdb-h的输出里有几个参数值得记住Page size决定了单页能放多少行也间接决定了 BLOB 的读取效率Oldest transaction就是 OATOldest active是 OIT两者相差几百以内算正常差到几十万说明有长事务在拖后腿Next transaction是一直递增的事务号涨到 20 亿附近会有溢出风险正规做法是做一次备份还原把事务号重置回低位。-d看的是数据页的填充率如果大量页只填了 30%说明删除和更新留下的空洞多该做一次gbak备份还原把空间收回来。页大小定下来之后想改唯一的办法是gbak备份再gbak -c -p 8192还原没有在线改的路径。所以建库前要想清楚如果表里有一堆VARCHAR(200)加上几个 BLOB 字段用 4096 或 8192 能少很多跨页读如果只是几百张配置表、单行很小1024 反而更省内存。单个文件大到一定程度就得拆成多文件-- 追加第二个数据文件起始页号接在主文件之后 ALTER DATABASE ADD FILE /opt/data/demo_02.gdb STARTING AT PAGE 200000 LENGTH 200000;这条语句里的STARTING AT PAGE是主文件的页号边界不是字节数写错会导致新文件永远用不上。我一般只在受文件系统单文件上限限制的场景下才拆拆完一定要用gstat -h确认Database files那一行变成了两个否则等于白写。2.3 服务形态本地直连还是 TCP 远程InterBase 6.0 的一个特点是同一个客户端库既能连本地文件也能连远程服务区别只在连接串怎么写。这一点经常被忽略然后在部署时踩坑——因为远程连接里那个路径是「服务端视角」的不是你自己机器上的路径。连接串写法走的协议适用场景C:\data\demo.gdb本地直连单机工具、免装服务端、数据不外发localhost:C:\data\demo.gdbTCP 回环本机调试但要验证服务端权限和锁192.168.1.20:C:\data\demo.gdbTCP 远程多人共用一台库服务器srv01:/opt/data/demo.gdbTCP 远程跨平台部署服务端是 Linux本地直连省掉了网络往返和权限校验启动快、部署简单很适合随应用程序一起分发的单机版。但它的并发模型比远程弱得多多个进程同时写同一个文件时锁冲突会很难排查所以只要业务里有第二个人同时写就别图省事老老实实装服务端走 TCP。远程连接的另一个前提是服务名要能解析。客户端库默认用gds_db这个服务名去找端口主机上必须有一行登记# 服务端和客户端的 services 文件里都要有这行端口默认 3050 gds_db 3050/tcp # InterBase 服务名与监听端口 # 改完确认端口真的在监听 netstat -an | grep 3050注意客户端报unavailable database时先别怀疑库文件损坏八成是这行没登记或者被防火墙拦了。3. 跑起来从服务启动到控件发出第一个查询3.1 安装与最小可用配置服务端装完之后要先确认三件事进程在跑、端口在听、SYSDBA能登进去。很多人卡在第一步是因为把客户端和服务端装混了——客户端只需要一个连接库Windows 上是gds32.dll服务端才有后台进程。# 1) 确认服务端进程存在 ps -ef | grep -i interbase # 2) 确认 3050 端口处于 LISTEN netstat -anp | grep 3050 # 3) 用命令行工具直连验证 SYSDBA 能过 isql -user SYSDBA -password masterkeyisql是随服务端一起提供的命令行客户端它同时也是一个 SQL 脚本执行器后面建库、灌表结构、跑迁移脚本都靠它。第三步如果提示用户名口令错误说明安装时设的SYSDBA口令不是默认值得找当时的部署记录如果提示unavailable database回到 2.3 节查服务名登记。Windows 上还有一个常见问题应用程序目录里的gds32.dll和系统目录里的版本不一致。客户端库的版本必须和服务端大致匹配混用不同版本时表现是能连上、能查询但一取 BLOB 或者一调存储过程就崩排查成本极高。我的习惯是把服务端自带的那份客户端库直接拷到 exe 同目录让它优先加载别指望系统的搜索路径。3.2 isql 建库建表跑通第一个库CREATE DATABASE是客户端工具语句不是服务端 SQL所以必须在isql里执行。路径同样按服务端视角写。# 建库页大小和字符集在这里定死后面改不了 isql -user SYSDBA -password masterkey EOF CREATE DATABASE /opt/data/demo.gdb USER SYSDBA PASSWORD masterkey PAGE_SIZE 8192 DEFAULT CHARACTER SET UNICODE_FSS; EOF # 灌表结构脚本开头先 CONNECT保证连的是目标库 isql -user SYSDBA -password masterkey -i schema.sqlschema.sql里第一句必须是CONNECT /opt/data/demo.gdb;后面才是 DDL。PAGE_SIZE和DEFAULT CHARACTER SET建库时定死UNICODE_FSS是 6.0 里能存多字节字符的字符集存中文就别用NONE否则排序和长度计算都会出问题。建表这块有两个 6.0 时代特有的写法值得用上DEFAULT NOW这种上下文变量以及 64 位整型主键。CONNECT /opt/data/demo.gdb; CREATE TABLE T_ORDER ( ID BIGINT NOT NULL, ORDER_NO VARCHAR(32) NOT NULL, AMOUNT NUMERIC(18,2) DEFAULT 0 NOT NULL, STATUS SMALLINT DEFAULT 0 NOT NULL, CREATED_AT TIMESTAMP DEFAULT NOW NOT NULL, CONSTRAINT PK_T_ORDER PRIMARY KEY (ID) ); -- 订单号唯一单独建索引查询走索引而不是全表 CREATE UNIQUE INDEX IDX_ORDER_NO ON T_ORDER (ORDER_NO);DEFAULT NOW是服务端求值的取的是数据库服务器时间不是客户端时间这一点在跨时区的多客户端场景里很关键——用客户端赋时间会出现同一批数据时间倒挂。BIGINT是 64 位整型只在 dialect 3 下可用用它做主键是为了避免几百万行之后INTEGER溢出。NUMERIC(18,2)在 dialect 3 下映射到 64 位整型存储精度不会丢做金额字段比DOUBLE PRECISION靠谱得多。3.3 控件对接连接、事务边界与参数化查询Delphi 那套控件里连 InterBase 最直接的是 IBXInterBase Express它不依赖 BDE直连客户端库部署时少一层中间件。核心是三个控件TIBDatabase管连接TIBTransaction管事务TIBQuery管数据集。// 数据库连接路径前缀决定走本地直连还是 TCP IBDB.DatabaseName : localhost:/opt/data/demo.gdb; IBDB.Params.Clear; IBDB.Params.Add(user_nameSYSDBA); IBDB.Params.Add(passwordmasterkey); IBDB.Params.Add(lc_ctypeUNICODE_FSS); // 必须和建库时一致 IBDB.SQLDialect : 3; // dialect 对不上会报类型转换错 IBDB.LoginPrompt : False; IBDB.Connected : True; // 事务显式控制别指望自动提交 IBTrans.DefaultDatabase : IBDB; IBTrans.Params.Clear; IBTrans.Params.Add(isc_tpb_write); // 读写事务 IBTrans.Params.Add(isc_tpb_concurrency); // 快照隔离读不阻塞写 IBTrans.Params.Add(isc_tpb_wait); // 遇锁等待而不是立刻抛异常 IBTrans.StartTransaction;lc_ctype是客户端字符集它必须和服务端建库时的DEFAULT CHARACTER SET对上对不上的表现是中文变问号或者VARCHAR(32)明明只存了 20 个汉字却报超长。isc_tpb_wait和isc_tpb_nowait的选择直接影响用户体验nowait遇到锁冲突立刻抛lock conflict on no wait transaction适合后台批处理快速失败wait会挂起等待适合交互界面。查询部分建议全部走参数化别拼字符串IBQuery1.Close; IBQuery1.SQL.Text : SELECT ID, ORDER_NO, AMOUNT FROM T_ORDER WHERE STATUS :S AND CREATED_AT :D ORDER BY ID; IBQuery1.ParamByName(S).AsInteger : 1; IBQuery1.ParamByName(D).AsDateTime : Date - 7; IBQuery1.Open; while not IBQuery1.Eof do begin // 读字段用 FieldByName别用下标后面加字段不会崩 Memo1.Lines.Add(IBQuery1.FieldByName(ORDER_NO).AsString); IBQuery1.Next; end; IBQuery1.Close; IBTrans.Commit; // 读事务也要提交不然 OAT 卡住最后那句Commit是重点。IBX 里TIBTransaction默认不自动提交一个TIBQuery打开又关掉事务仍然是活的它对应的快照事务号就一直挂在 OIT 上。前面说的库文件疯长源头往往就是这种「只读了一下」的代码。如果一次操作要写多张表用TIBUpdateSQL配合TIBQuery的缓存更新把提交点收在业务动作结束那一刻比一条 SQL 一次提交安全得多。4. 核心模块拆解生成器、触发器、存储过程与 UDF4.1 生成器与主键发号GEN_ID 的正确用法生成器Generator是 InterBase 里的独立计数器和表无关专门用来发号。CREATE GENERATOR GEN_ORDER_ID; SET GENERATOR GEN_ORDER_ID TO 0; -- 取下一个号第二个参数是步长 SELECT GEN_ID(GEN_ORDER_ID, 1) FROM RDB$DATABASE;GEN_ID的第二个参数是增量正常情况下传 1需要一次预留一段号比如批量导入时传 N。RDB$DATABASE是系统表永远只有一行用来把函数当查询跑。这里有一个必须记住的性质生成器不受事务控制。取号这个动作在事务回滚后不会退回所以号一定有空洞。很多人拿它直接当订单号展示给用户看结果发现编号不连续被业务方追着问。我的做法是生成器只做物理主键业务单号单独用「日期 序列」拼序列另有自己的表去维护。反过来如果业务真的要求连号那就不能靠生成器得在一张T_SEQ表上做UPDATE ... SET CUR CUR 1并配合行锁代价是并发性能直线下降这个取舍要提前跟业务说清楚。4.2 触发器把业务约束下沉到数据库层触发器在 InterBase 里分 BEFORE 和 AFTER还分行级和语句级。行级触发器里能用NEW和OLD两个上下文变量分别代表新值和旧值。-- 自定义异常抛出时客户端能拿到这条消息 CREATE EXCEPTION E_AMOUNT_INVALID 订单金额必须大于 0; -- 主键发号BEFORE INSERT在约束检查之前把 ID 填上 CREATE TRIGGER TR_ORDER_BI FOR T_ORDER ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.ID IS NULL) THEN NEW.ID GEN_ID(GEN_ORDER_ID, 1); END; -- 业务校验POSITION 1 保证排在发号之后执行 CREATE TRIGGER TR_ORDER_BIU FOR T_ORDER ACTIVE BEFORE INSERT OR UPDATE POSITION 1 AS BEGIN IF (NEW.AMOUNT 0) THEN EXCEPTION E_AMOUNT_INVALID; END;POSITION决定同一个表上多个同类触发器的执行顺序编号小的先跑。上面这两个触发器必须按 0、1 排否则校验里拿到的NEW.ID还是空。EXCEPTION抛出去之后客户端TIBQuery会收到一条EIBErrorMessage里带的就是E_AMOUNT_INVALID那条文本这是把校验规则放在数据库层最实际的好处——不管前端是 Delphi、Java 还是命令行工具规则都跑不掉。需要注意BEFORE 触发器里改NEW的字段是有效的AFTER 触发器里改就没有意义了因为记录已经落盘。另外语句级触发器FOR EACH STATEMENT在行级操作里拿不到NEW/OLD它主要用来POST_EVENT做业务逻辑时千万别用它。4.3 可选中存储过程与事件通知存储过程分两种一种用EXECUTE PROCEDURE调用、只做副作用另一种带SUSPEND可以被SELECT当表查叫可选中存储过程。CREATE PROCEDURE SP_ORDER_LIST (P_STATUS SMALLINT) RETURNS (ID BIGINT, ORDER_NO VARCHAR(32), AMOUNT NUMERIC(18,2)) AS BEGIN FOR SELECT O.ID, O.ORDER_NO, O.AMOUNT FROM T_ORDER O WHERE O.STATUS :P_STATUS ORDER BY O.ID INTO :ID, :ORDER_NO, :AMOUNT DO SUSPEND; -- 每处理一行挂起一次客户端才能逐行取到 END;SUSPEND是这段代码的核心。没有它存储过程会把所有行算完只返回最后一组值有了它服务端每算出一行就挂起客户端取一行再唤醒内存占用是常数级别的。FOR SELECT ... INTO里的变量名前必须加冒号这是 PSQL 里区分「字段名」和「变量名」的语法漏了冒号会报「列名不明确」。客户端调用有两种// 方式一当表查最灵活能带排序和过滤 IBQuery1.SQL.Text : SELECT * FROM SP_ORDER_LIST(:S) ORDER BY ID; IBQuery1.ParamByName(S).AsInteger : 1; IBQuery1.Open; // 方式二TIBStoredProc适合无返回集的存储过程 IBSP.StoredProcName : SP_ORDER_CLOSE; IBSP.ParamByName(P_ID).AsLargeInt : 1024; IBSP.ExecProc;配套的还有事件机制用来做「数据变了通知所有客户端刷新」。服务端在触发器里POST_EVENT客户端注册同名事件CREATE TRIGGER TR_ORDER_EVENT FOR T_ORDER ACTIVE AFTER INSERT OR UPDATE POSITION 0 AS BEGIN POST_EVENT ORDER_CHANGED; END;IBEvents1.Database : IBDB; IBEvents1.Events.Text : ORDER_CHANGED; // 多个事件名用换行分隔 IBEvents1.OnEventAlert : DoOrderChanged; IBEvents1.Registered : True; // 注册之后服务端才会推POST_EVENT的名字建议用大写短名老版本对事件名长度限制得比较紧不要写成一句描述。更关键的是OnEventAlert回调是在客户端库的后台线程里触发的在里面直接操作 VCL 控件会因为跨线程访问出玄学崩溃正确做法是TThread.Queue或者PostMessage把刷新动作丢回主线程。我第一次用事件通知时就在这里翻过车表现为「偶尔」崩查了两天才定位到线程上。4.4 UDF拿到源码后自己编一个外部函数库内置函数不够用时InterBase 支持外部函数UDF把 C 函数编成动态库在数据库里声明之后就能在 SQL 里调用。/* myudf.c —— 编成动态库后由服务端加载 */ #include string.h /* 参数用 CSTRING 声明时收到的是以 \0 结尾的字符串指针 */ long STRLEN_UDF(char *s) { if (s NULL) return 0; return (long) strlen(s); }# Linux 下编成 .so位数必须和服务端一致32 位服务端只认 32 位库 gcc -shared -fPIC -O2 -o myudf.so myudf.c # Windows 下编成 DLL cl /LD myudf.c /Fe:myudf.dll编出来之后丢到服务端的 UDF 目录然后在库里声明DECLARE EXTERNAL FUNCTION STRLEN_UDF CSTRING(256) RETURNS INTEGER BY VALUE ENTRY_POINT STRLEN_UDF MODULE_NAME myudf;三个参数各有讲究CSTRING(256)告诉服务端按定长字符串传参如果用VARCHAR(256)声明传进去的是带长度前缀的结构体指针函数签名就得跟着改BY VALUE表示返回值按值传整型必须加这个漏了会读到一个野指针ENTRY_POINT是导出符号名MODULE_NAME是不带扩展名的库名。改完库之后必须重启服务端连接才会重新加载改完函数不重启是最常见的「明明改了却没生效」。5. 避坑与排查五个把项目拖进坑里的细节5.1 恢复之后主键冲突集中爆发现象从生产库备份出来恢复到测试环境应用一插数据就报violation of PRIMARY or UNIQUE KEY constraint但表里明明只有几百行。原因gbak做完整备份还原时会把生成器的当前值一起带过去但如果这次恢复用的是只导结构的 DDL 脚本或者用-m只恢复元数据生成器的值就回到了建库时的初始状态。而数据行是从别处灌进来的ID已经用到了几千两边一撞就冲突。解决恢复完之后先对齐每个生成器再让应用跑起来。-- 把生成器重置到当前最大 ID批量做的时候用脚本生成这段 SET GENERATOR GEN_ORDER_ID TO 0; SELECT GEN_ID(GEN_ORDER_ID, COALESCE(MAX(ID), 0)) FROM T_ORDER;第二句借GEN_ID的步长参数一次性把计数器顶到MAX(ID)比SET GENERATOR ... TO 具体数字少一次人工查数。我现在的习惯是恢复脚本最后必须带这一段每个生成器一行缺一个就等着线上报冲突。5.2 库文件只涨不缩sweep 跑不动现象删了几百万行历史数据磁盘占用一点没降查询一天比一天慢跑gfix -sweep跑了几个小时还在跑。原因删除在 MGA 里只是把版本标记成死真正回收要靠 sweep。而 sweep 能推进的前提是 OAT 能往前走只要有一个长事务不提交回收立刻停摆。第二个原因是 sweep 本身是协作式的它和正常业务抢资源业务压力大时会一直退让。解决先查 OIT 和 OAT 的差距定位长事务再决定是手动 sweep 还是干脆做一次备份还原。# 看水位线差距差几十万说明有长事务 gstat -h /opt/data/demo.gdb | grep -i oldest # 手动触发回收最好放在业务低峰 gfix -sweep /opt/data/demo.gdb -user SYSDBA -password masterkey # 彻底方案备份还原空间能真正收回来 gbak -b /opt/data/demo.gdb /tmp/demo.fbk -user SYSDBA -password masterkey -v gbak -c -p 8192 /tmp/demo.fbk /opt/data/demo_new.gdb -user SYSDBA -password masterkey -vsweep只回收页内空间文件大小不会缩只有备份还原才会重写整个文件、把空间真正交还给文件系统。所以计划里应该把「定期备份还原」当成运维动作而不是只当灾备。5.3 Dialect 混用DATE 变成 TIMESTAMP双引号变成标识符现象客户端查出来的日期字段多出了时间部分一条WHERE NAME ABC忽然报「列名不明确」NUMERIC(18,2)的金额在小数位上丢精度。原因这是 dialect 1 和 dialect 3 的语义差异。dialect 1 里DATE实际是带时间的 TIMESTAMP双引号包起来的是本文还有配套的精品资源点击获取