15个Firebird反模式
Alexey Kovyazin 撰写,2025年1月14日
引言
本文档概述了使用 Firebird 数据库时常见的 15 个反模式,并为每个模式提供了解决方案。
1. 对 MON$ 的多个并行查询
反模式: 一个非常常见的错误–在 OnConnect 触发器中查询 MON$ATTACHMENTS 以选择用户详细信息用于审计目的,或计算连接数用于许可目的。
为什么不好?
-
MON$ 表是存储在 fbNN_mon_xx 系统文件中的虚拟表,包含性能统计等信息
-
文件 >1Gb 意味着你使用得太频繁了
-
它们仅设计用于系统管理员使用–即 1-2 个并行查询,且仅供管理员使用
-
200+ 个连接并行查询 MON$ 会显著降低 Firebird 的速度,500+ 个同时查询将极有可能导致 Firebird “挂起”
解决方案:
-
不要将 MON$ 用于非管理任务,即计数或审计,避免在 OnConnect 中使用它们
-
对于审计目的:
-
使用上下文变量,如 CURRENT_USER、CURRENT_TIMESTAMP 等
-
使用 Audit–Firebird 原生功能,比触发器强大得多
-
-
对于许可目的–使用用户的上下文变量
2. 仪表板加载缓慢
反模式: 在应用程序启动时加载综合仪表板或记分板,汇总上个月或去年的所有订单和发票,或每分钟或更频繁地更新某些指标。
SELECT
SUM(total_sales) as yearly_sales,
COUNT(DISTINCT customers) as customer_count,
AVG(order_value) as avg_order_value
FROM orders
WHERE order_date BETWEEN '2025-01-01' AND '2025-01-01';
为什么不好?
-
用户必须等待几秒钟才能看到公司范围的统计数据,然后才能开始实际工作
-
从 Firebird 的角度来看–为了持续运行许多并行查询、检索大量数据、排序/分组,Firebird 将密集使用多个 CPU 核心,从磁盘、缓存、专用于排序的内存中读取(有时排序会转到磁盘)
-
这就像每分钟构建多次报告!
解决方案:
-
减少能看到仪表板的用户数量:
-
通常仪表板仅对分析师和管理层需要,将其从常规应用程序加载中排除
-
使仪表板在启动时/某个表单上的加载为可选,默认禁用
-
通过显式按钮点击加载仪表板数据,而不是在启动时(即将其作为报告)
-
-
通过计划(即机器人)用 1 个进程计算仪表板数据,并将其存储到简单表中,以便通过简单查询检索
-
使用触发器聚合数据并存储以备使用
-
使用副本数据库计算仪表板数据(以及所有重型报告)
3. 加载不必要的数据记录
反模式: 打开应用程序或表单时加载所有数据而不进行过滤到网格中,无论其中是否包含数十万条记录。
procedure TDataForm.LoadAllRecords;
begin
FDQuery1.SQL.Text := 'SELECT * FROM large_table';
FDQuery1.Open;
// 将整个表加载到内存中
DBGrid1.DataSource.DataSet := FDQuery1;
end;
为什么不好?
-
尽管网格只显示 50 条记录,用户必须滚动浏览数千条记录,而不是使用搜索功能
-
在 99% 的情况下,用户需要的是非常窄的数据子集:例如,最近的销售记录
-
从 Firebird 的角度来看:
-
每次打开都需要读取、存储在缓存中,并通过网络传输数千条记录
-
如果你保持数据集打开(在 Delphi 中),Firebird 会保留缓冲区、临时空间中的排序记录(如果有 ORDER BY、GROUP BY 等),直到数据集关闭
-
解决方案:
-
使用 FIRST/SKIP/ROWS 限制记录数
-
使用某些条件限制记录数,例如,显示最近 3 天内创建/更改的记录
-
通常,尽快关闭查询。
4. 滚动时过度查询
反模式: 在滚动事件上执行查询。例如,在网格或表格中显示数据时,为每条记录执行单独的查询,或者如果你使用经典的 2 个网格中主从滚动示例而没有延迟。
procedure TForm1.GridScrolled(Sender: TObject);
begin
// 为每行查询
FDQuery2.SQL.Text :=
'SELECT additional_info FROM details ' +
'WHERE id = ' + IntToStr(CurrentRowId);
FDQuery2.Open;
end;
为什么不好?
-
在动态网格中为每条记录执行单独的查询,迫使 Firebird 处理数千个小查询,不必要地消耗 CPU 资源
-
从 Firebird 的角度来看:
- 许多(每秒数千个)小查询将产生显著的 CPU 负载,因为即使查询在统计中显示 0ms,它也需要准备、执行、传输结果等
解决方案:
-
使用批量操作一次加载多行
-
增强网格的主查询,将详细查询作为其一部分执行
-
添加显式按钮以加载网格可见部分的详细信息
-
添加延迟以执行获取详细信息的查询,防止滚动期间立即查询
-
默认情况下不要为所有用户启用滚动加载详细信息
5. 不必要自动刷新
反模式: 在每个客户端应用程序中以最小间隔自动刷新网格数据,且此功能默认启用。
为什么不好?
-
这导致数百个客户端连接运行几乎相同的查询来检索相同记录
-
发生位置:计划的自动刷新、队列位置的选择、搜索“最近时段”等
-
从 Firebird 的角度来看:
- 仪表板加载和滚动事件的组合:许多中等大小的查询对系统产生负载
解决方案:
-
增加间隔!
-
实现显式(用户触发的)刷新
-
基于实际数据变化使用选择性数据集刷新(流式或触发器或事件+流式)
6. 频繁更新记录
反模式: 在不同事务中频繁更新同一记录,创建大量记录版本。
为什么不好?
-
具有数十个版本的记录会显著降低性能,具有数千个版本的记录可能成为阻塞点
-
从 Firebird 的角度来看:必须重建记录版本链以识别特定事务的正确版本,这需要大量读操作,结果垃圾回收变得显著更慢。
解决方案:
-
迁移到 Firebird 4+,其中有中间垃圾回收
-
不要保持长时间运行的可写事务,进行适当的垃圾回收
-
对于 Firebird <4,考虑使用 DELETE+INSERT 代替 UPDATE
7. 对只读选择使用写事务
反模式: 对只读选择使用写事务会导致过多操作。
为什么不好?
-
对只读选择使用写事务会导致许多不必要的头页写入
-
对只读操作使用写事务效率低下(提交时大的 TIP 会给服务器带来额外负载)
解决方案:
-
对不更改数据的操作使用单独的只读事务
-
Firebird 是少数允许在单个连接框架内打开多个事务的数据库之一
-
全局临时表可用于只读事务
8. 使用 LIKE :param
以下带参数的查询将不会对 fieldName 使用索引(即使存在索引):
SELECT * FROM Table1 WHERE fieldName LIKE :param1
为什么不好?
由于 LIKE 允许通配符搜索(%),它可以替换任意数量的符号,Firebird 无法预先确定参数值是否适合索引搜索。
通常开发者尝试通过将参数值嵌入查询文本来解决此问题:
-
fieldName LIKE «Alex%» - 可能使用索引
-
fieldName LIKE «%Alex» - 无法使用标准索引
-
fieldName LIKE «%Alex%» - 完全无法使用索引
这会引发其他问题(见下方第10条)。
解决方案:
1. 对于已知字符串前缀,使用 STARTING WITH
当搜索值永远不会以通配符 % 开头时,优先使用 STARTING WITH 而不是 LIKE:
WHERE fieldName STARTING WITH ?param1
2. 优化双向字符串搜索
对于具有已知前缀或后缀模式的字符串,使用反向索引:
-- 创建反向索引
CREATE INDEX ixreverse1 ON TABLE1 COMPUTED BY (REVERSE(fieldName));
-- 使用两个方向进行查询
WHERE fieldName STARTING WITH :param1
OR reverse(fieldName) STARTING WITH reverse(:param2)
3. 实施渐进式搜索策略
对于出现在开头/结尾/中间(但不同时出现)的字符串:
-
首先尝试使用 STARTING WITH 进行快速索引搜索
-
如果没有找到结果,则回退到较慢的 LIKE 搜索
4. 基于单词的搜索优化
当搜索完整单词(以空格、逗号等分隔)时:
-
创建单独的单词-ID映射表
-
通过映射表进行搜索,而不是直接搜索原始文本
5. 对于全面的全文搜索功能:
-
考虑使用 IBSurgeon Full Text Search UDR
-
这个开源解决方案提供了高级文本搜索功能
9. 只读操作未关闭事务
为什么不好?
- 长时间保持事务打开可能会迫使 Firebird 为潜在的快照事务维护大量旧版本
解决方案:
-
尽可能使用只读事务,并尽快关闭可写事务
-
使用现代 Firebird 版本(4+)以减少记录版本链的影响
-
实施适当的 sweep
10. 查询参数化问题
反模式: 避免使用预编译查询和参数化,而是将参数值直接嵌入查询文本中。
FDQuery1.SQL.Text :=
'SELECT * FROM users WHERE name = ''' +
EditUsername.Text + '''';
FDQuery1.Open;
为什么不好?
-
这种做法会降低重复查询的性能
-
每个嵌入参数值的查询都需要重新准备
-
对于大型表,准备过程可能耗时较长
-
使问题分析复杂化
-
难以按文本对查询进行分组
-
造成 SQL 注入漏洞
解决方案:
FDQuery1.SQL.Text :=
'SELECT * FROM users WHERE name = :username';
FDQuery1.ParamByName('username').AsString :=
EditUsername.Text;
FDQuery1.Open;
11. 错误的完整性检查:使用触发器/CHECK 而不是主键
反模式: 使用触发器或 CHECK 而不是主键来进行数据库完整性检查。
为什么不好?
-
这忽略了主键验证使用特殊模式读取记录的当前版本,而不受用户事务隔离级别的影响。
-
在用户事务中使用触发器进行主键检查会增加重复的可能性,并 unnecessarily 使逻辑复杂化
解决方案:
-
使用主键
-
避免冗余的完整性检查
-
保持数据库逻辑简单
12. 使用 MAX() 生成 ID
反模式: 使用 MAX(id)+1 生成新标识符既不可靠也不高效。
INSERT INTO users (id, name)
VALUES ((SELECT MAX(id) + 1 FROM users),
'John Doe');
为什么不好?
-
使用 MAX(id)+1 而不是序列(生成器)来生成新标识符
-
在常见事务参数下,MAX(id)+1 无法保证唯一性 - 两个并行事务可能获得相同的 MAX() 值
-
Max()+1 与 CHECK(select if unique) 的组合也不起作用!
解决方案:
-- 使用生成器/序列!
CREATE GENERATOR gen_user_id;
-- 使用生成器生成 ID
INSERT INTO users (id, name)
VALUES (
GEN_ID(gen_user_id, 1),
'John Doe' );
13. 低效的 GUID 使用
为什么不好?
-
使用系统生成的 GUID 而不是 gen_uuid() 可能会影响索引性能
-
系统生成的 GUID 高度随机化
解决方案:
-
使用 gen_uuid() 函数
-
考虑使用 BIGINT 代替
-
在版本 6 中将提供 UUID v7
14. 低效的计算字段
反模式: 使用带有对其他表 SELECT 的计算字段会显著降低简单 SELECT 操作的性能。
CREATE TABLE orders (
id INTEGER,
total_amount COMPUTED BY (
(SELECT SUM(item_price) FROM order_items
WHERE order_items.order_id = orders.id)));
为什么不好?
-
计算字段是即时计算的,不适合实现复杂逻辑,并且可能显著增加优化难度
-
它加强了表之间的关系
-
计算字段仅适用于对表字段进行轻量级计算,例如字符串拼接
解决方案:
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
cached_total_amount DECIMAL(10,2));
CREATE TRIGGER update_order_total
BEFORE INSERT OR UPDATE ON orders
AS
BEGIN
NEW.cached_total_amount = (
SELECT SUM(item_price)
FROM order_items
WHERE order_items.order_id = NEW.id
);
END;
15. 无日志的错误抑制
反模式: 不要在没有日志记录的情况下抑制 Firebird 错误和警告!
try
FDQuery1.Open;
except
// 静默失败
end;
为什么不好?
- 隐藏错误会妨碍正确的诊断和调试。正确的错误日志记录对于快速理解和解决问题至关重要。
解决方案:
try
FDQuery1.Open;
except
on E: Exception do
begin
// 全面日志记录
Logger.Error('数据库连接失败:' + E.Message);
ShowMessage('无法连接到数据库。请联系支持人员。');
// 记录附加上下文
Logger.LogStackTrace(E);
end;
end;
联系信息
-
将您的问题发送至 [email protected]
-
成为 Firebird 支持者(每月10欧元起)并参与封闭的高级网络研讨会!
-
https://store.firebirdsql.org/p/firebird-associate-donation-eur/