15 Antipatrones de Firebird
por Alexey Kovyazin, 14-Ene-2025
Introducción
Este documento describe 15 anti-patrones comunes al trabajar con bases de datos Firebird y proporciona soluciones para cada uno.
1. Consultas paralelas múltiples a MON$
Anti-patrón: Un error muy popular - disparador OnConnect, consulta a MON$ATTACHMENTS para seleccionar los detalles del usuario con fines de auditoría, o calcular el número de conexiones con fines de licenciamiento.
¿Por qué es malo?
-
Las tablas MON$ son tablas virtuales que se almacenan en archivos de sistema fbNN_mon_xx, con estadísticas de rendimiento, etc.
-
Archivo >1Gb significa que lo estás usando demasiado
-
Están diseñadas solo para uso de administradores de sistemas - es decir, 1-2 consultas paralelas, exclusivamente para administradores
-
200+ conexiones con consultas paralelas a MON$ ralentizarán Firebird de manera muy significativa, y 500+ consultas simultáneas “colgarán” Firebird con alta probabilidad
Soluciones:
-
No uses MON$ para tareas no administrativas, es decir, para contar o auditar, evita usarlas en OnConnect
-
Para fines de auditoría:
-
Usa Variables de Contexto como CURRENT_USER, CURRENT_TIMESTAMP, etc.
-
Usa Auditoría - característica nativa de Firebird, mucho más potente que los disparadores
-
Para fines de licenciamiento - usa las variables de contexto del usuario
2. Carga Lenta del Panel de Control
Anti-patrón: Cargar paneles de control o marcadores completos que suman todos los pedidos y facturas del último mes o año durante el inicio de la aplicación, o actualizar algunas métricas cada minuto o más frecuentemente.
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';
¿Por qué es malo?
-
Los usuarios deben esperar varios segundos para ver estadísticas de toda la empresa antes de poder comenzar su trabajo real
-
Desde el punto de vista de Firebird - para ejecutar constantemente muchas consultas paralelas, recuperar grandes cantidades de datos, ordenarlos/agruparlos, Firebird usará intensivamente múltiples núcleos de CPU, leyendo del disco, caché, memoria dedicada para ordenamiento (y a veces el ordenamiento va al disco)
-
¡Es como construir un informe varias veces por minuto!
Soluciones:
- Disminuye el número de usuarios que verán los paneles de control:
-
Generalmente el Panel de Control solo es necesario para analistas y gerencia, exclúyelo de la carga general de la aplicación
-
Haz que la carga del panel de control al inicio/para algún formulario sea opcional, deshabilitada por defecto
-
Carga los datos del panel de control mediante un clic explícito en un botón, no al inicio (es decir, conviértelo en un informe)
-
Calcula los datos del panel de control con 1 proceso según un horario (es decir, un robot) y guárdalos en una tabla simple lista para ser recuperada mediante una consulta simple
-
Usa disparadores para agregar datos y almacenarlos listos para su uso
-
Usa una base de datos réplica para calcular los datos de los paneles de control (y también todos los informes pesados)
3. Carga de Registros Innecesarios
Anti-patrón: Cargar todos los datos sin filtrar en la cuadrícula al abrir una aplicación o formulario, independientemente de si contiene cientos de miles de registros.
procedure TDataForm.LoadAllRecords;
begin
FDQuery1.SQL.Text := 'SELECT * FROM large_table';
FDQuery1.Open;
// Carga toda la tabla en memoria
DBGrid1.DataSource.DataSet := FDQuery1;
end;
¿Por qué es malo?
-
A pesar de que la cuadrícula solo muestra 50 registros, los usuarios deben desplazarse por miles de registros en lugar de usar la funcionalidad de búsqueda
-
En el 99% de los casos los usuarios necesitan un subconjunto muy reducido de datos: los registros de ventas más recientes, por ejemplo
-
Desde el punto de vista de Firebird:
-
Cada apertura requiere lectura, almacenamiento en caché y transferencia de miles de registros a través de la red
-
Si mantienes el conjunto de datos abierto (en Delphi), Firebird mantiene buffers, registros ordenados en espacio temporal (si hay ORDER BY, GROUP BY, etc.) hasta el cierre del conjunto de datos
Soluciones:
-
Limita el número de registros con FIRST/SKIP/ROWS
-
Limita el número de registros con algún criterio, por ejemplo, mostrar registros creados/cambiados durante los últimos 3 días
-
En general, cierra las consultas lo antes posible.
4. Consultas Excesivas al Desplazarse
Anti-patrón: Ejecutar consultas en eventos de desplazamiento. Por ejemplo, al mostrar datos en una cuadrícula o tabla, realizar una consulta separada PARA CADA registro, o si usas el ejemplo clásico de desplazamiento maestro-detalle en 2 cuadrículas sin demora.
procedure TForm1.GridScrolled(Sender: TObject);
begin
// consulta para cada fila
FDQuery2.SQL.Text :=
'SELECT additional_info FROM details ' +
'WHERE id = ' + IntToStr(CurrentRowId);
FDQuery2.Open;
end;
¿Por qué es malo?
-
Realizar una consulta separada PARA CADA registro en una cuadrícula dinámica obliga a Firebird a procesar miles de consultas diminutas, consumiendo recursos de CPU innecesariamente
-
Desde el punto de vista de Firebird:
-
Muchas (miles por segundo) consultas pequeñas crearán una carga significativa de CPU, porque incluso si la consulta muestra 0ms en las estadísticas, requiere ser preparada, ejecutada, transferir el resultado, etc.
Soluciones:
-
Carga múltiples filas a la vez usando operaciones por lotes
-
Mejora la consulta principal de la cuadrícula para ejecutar la consulta detallada como parte de ella
-
Agrega un botón explícito para cargar detalles de la parte visible de la cuadrícula
-
Agrega una demora para ejecutar la consulta que recibe los detalles, para evitar consultas inmediatas durante el desplazamiento
-
No habilites la carga de detalles al desplazarse para todos los usuarios por defecto
5. Actualizaciones Automáticas Innecesarias
Anti-patrón: Actualizar los datos de la cuadrícula automáticamente a intervalos mínimos en cada aplicación cliente, con esta característica habilitada por defecto.
¿Por qué es malo?
-
Esto resulta en cientos de conexiones de clientes ejecutando consultas casi idénticas para recuperar los mismos registros
-
Dónde ocurre: actualizaciones automáticas de horarios, o selección de posiciones en cola, o búsqueda de “espacio más cercano”, etc.
-
Desde el punto de vista de Firebird:
-
Combinación de paneles de control y eventos de desplazamiento: muchas consultas de tamaño mediano crean carga en el sistema
Soluciones:
-
¡Aumenta el intervalo!
-
Implementa actualizaciones explícitas (activadas por el usuario)
-
Usa actualizaciones selectivas del conjunto de datos basadas en cambios reales de datos (streaming o disparadores o evento+streaming)
6. Actualizaciones Frecuentes de Registros
Anti-patrón: Actualizar frecuentemente el mismo registro en diferentes transacciones, creando numerosas versiones de registros.
¿Por qué es malo?
-
Un registro con docenas de versiones puede degradar significativamente el rendimiento, un registro con miles puede convertirse en un bloqueador
-
Desde el punto de vista de Firebird: la cadena de versiones de registros debe reconstruirse para identificar la versión correcta de la transacción específica, requiere numerosas operaciones de lectura, y como resultado, la recolección de basura se vuelve significativamente más lenta.
Soluciones:
-
Migra a Firebird 4+, hay recolección de basura intermedia
-
No mantengas transacciones de escritura de larga duración, realiza la recolección de basura adecuada
-
Para Firebird <4, considera usar DELETE+INSERT en lugar de UPDATE
7. Uso de transacciones de escritura para selecciones de solo lectura
Anti-patrón: Usar transacciones de escritura para selecciones de solo lectura conduce a operaciones excesivas.
¿Por qué es malo?
-
Usar transacciones de escritura para selecciones de solo lectura conduce a muchas escrituras innecesarias de páginas de cabecera
-
Usar transacciones de escritura para operaciones de solo lectura es ineficiente (TIP grande durante el commit crea carga adicional en el servidor)
Soluciones:
-
Usa una transacción separada de solo lectura para operaciones que no cambian datos
-
Firebird es una de las pocas bases de datos que permite abrir varias transacciones en el marco de una sola conexión
-
Las Tablas Temporales Globales están disponibles para su uso en transacciones de solo lectura
8. Uso de LIKE :param
La siguiente consulta con parámetro no usará índice para el nombre del campo (incluso si existe un índice):
SELECT * FROM Table1 WHERE fieldName LIKE :param1
¿Por qué es malo?
Dado que LIKE permite búsqueda con comodines (%), que pueden reemplazar cualquier número de símbolos, Firebird no puede determinar de antemano si el valor del parámetro será adecuado para la búsqueda por índice.
Generalmente los desarrolladores intentan solucionarlo incrustando el valor del parámetro en el texto de la consulta:
-
fieldName LIKE «Alex%» - posible usar índice
-
fieldName LIKE «%Alex» - no es posible usar índice estándar
-
fieldName LIKE «%Alex%» - no es posible usar índice en absoluto
Conduce a otros problemas (ver #10 abajo).
Soluciones:
1. Usa STARTING WITH para prefijos de cadena conocidos
Cuando tu valor de búsqueda nunca comienza con un comodín %, prefiere STARTING WITH sobre LIKE:
WHERE fieldName STARTING WITH ?param1
2. Optimiza búsquedas de cadenas bidireccionales
Para cadenas con patrones de prefijo o sufijo conocidos, usa índice invertido:
-- Crear índice invertido
CREATE INDEX ixreverse1 ON TABLE1 COMPUTED BY (REVERSE(fieldName));
-- Consulta usando ambas direcciones
WHERE fieldName STARTING WITH :param1
OR reverse(fieldName) STARTING WITH reverse(:param2)
3. Implementa estrategia de búsqueda progresiva
Para cadenas que aparecen al inicio/final/medio (pero no simultáneamente):
-
Primero intenta búsqueda indexada rápida con STARTING WITH
-
Si no se encuentran resultados, recurre a búsqueda más lenta con LIKE
4. Optimización de Búsqueda Basada en Palabras
Al buscar palabras completas (delimitadas por espacios, comas, etc.):
-
Crea una tabla separada de mapeo palabra-ID
-
Busca a través de la tabla de mapeo en lugar del texto original
5. Para capacidades integrales de búsqueda de texto completo:
-
Considera usar IBSurgeon Full Text Search UDR
-
Esta solución de código abierto proporciona funcionalidad avanzada de búsqueda de texto
9. No cerrar transacciones para operaciones de solo lectura
¿Por qué es malo?
- Mantener transacciones abiertas por períodos prolongados podría obligar a Firebird a mantener numerosas versiones anteriores para posibles transacciones de instantánea
Soluciones:
-
Usa transacciones de solo lectura cuando sea posible, y cierra las transacciones de escritura lo antes posible
-
Usa versiones modernas de Firebird (4+) para reducir el impacto de las cadenas de versiones de registros
-
Implementa un sweep adecuado
10. Problemas de Parametrización de Consultas
Anti-patrón: Evitar consultas preparadas y parametrización, incrustando en su lugar los valores de los parámetros directamente en el texto de la consulta.
FDQuery1.SQL.Text :=
'SELECT * FROM users WHERE name = ''' +
EditUsername.Text + '''';
FDQuery1.Open;
¿Por qué es malo?
-
Esta práctica reduce el rendimiento para consultas repetidas
-
Cada consulta con valores de parámetros incrustados debe prepararse como nueva
-
La preparación puede ser larga y consumir tiempo para tablas grandes
-
Complica el análisis de problemas
-
Es difícil agrupar consultas por texto
-
Crea vulnerabilidades de inyección SQL
Soluciones:
FDQuery1.SQL.Text :=
'SELECT * FROM users WHERE name = :username';
FDQuery1.ParamByName('username').AsString :=
EditUsername.Text;
FDQuery1.Open;
11. Verificación de Integridad Incorrecta: disparadores/CHECKs en lugar de Clave Primaria
Anti-patrón: Usar disparadores o CHECK en lugar de Claves Primarias para verificaciones de integridad de la base de datos.
¿Por qué es malo?
-
Esto ignora que la validación de Clave Primaria usa el modo especial para leer la versión actual del registro, independientemente del nivel de aislamiento de transacción del usuario.
-
Hacer verificaciones de PK con disparadores en transacciones de usuario aumenta la posibilidad de duplicación y complica innecesariamente la lógica
Soluciones:
-
Usa claves primarias
-
Evita verificaciones de integridad redundantes
-
Mantén la lógica de la base de datos simple
12. Generación de ID con MAX()
Anti-patrón: Usar MAX(id)+1 para nuevos identificadores es poco confiable e ineficiente.
INSERT INTO users (id, name)
VALUES ((SELECT MAX(id) + 1 FROM users),
'John Doe');
¿Por qué es malo?
-
Usar MAX(id)+1 en lugar de secuencias (generadores) para nuevos identificadores
-
MAX(id)+1 no garantiza unicidad con parámetros de transacción comunes - dos transacciones paralelas podrían recibir el mismo valor MAX()
-
¡La combinación de Max()+1 y CHECK(select si es único) tampoco funciona!
Soluciones:
-- ¡Usa generador/secuencia!
CREATE GENERATOR gen_user_id;
-- Usa generador para la generación de ID
INSERT INTO users (id, name)
VALUES (
GEN_ID(gen_user_id, 1),
'John Doe' );
## 13. Uso Ineficiente de GUID
**¿Por qué es malo?**
- Usar GUID generados por el sistema en lugar de gen\_uuid() puede afectar el rendimiento de los índices
- El GUID generado por el sistema es altamente aleatorio
**Soluciones:**
- Usar la función gen\_uuid()
- Considerar usar BIGINT en su lugar
- En la versión 6 habrá UUID v7
## 14. Campos Calculados Ineficientes
**Anti-patrón:** Usar campos calculados con SELECTs a otras tablas disminuye significativamente el rendimiento de operaciones SELECT simples.
```sql hljs
CREATE TABLE orders (
id INTEGER,
total_amount COMPUTED BY (
(SELECT SUM(item_price) FROM order_items
WHERE order_items.order_id = orders.id)));
¿Por qué es malo?
-
Los campos calculados se calculan sobre la marcha, y no están destinados a implementar lógica compleja, y pueden complicar significativamente los esfuerzos de optimización
-
Fortalece las relaciones entre tablas
-
Tiene sentido usar campos calculados solo para cálculos ligeros con campos de la tabla, como concatenación
Soluciones:
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. Supresión de Errores Sin Registro
Anti-patrón: ¡No suprima los errores y advertencias de Firebird sin registrarlos!
try
FDQuery1.Open;
except
// Falla silenciosa
end;
¿Por qué es malo?
- Ocultar errores impide un diagnóstico y depuración adecuados. El registro adecuado de errores es crucial para comprender y resolver problemas rápidamente.
Soluciones:
try
FDQuery1.Open;
except
on E: Exception do
begin
// Registro completo
Logger.Error('Falló la conexión a la base de datos: ' + E.Message);
ShowMessage('No se puede conectar a la base de datos. Por favor, contacte con soporte.');
// Registrar contexto adicional
Logger.LogStackTrace(E);
end;
end;
Información de Contacto
-
Envíe sus preguntas a [email protected]
-
¡Conviértase en Firebird Supporter (desde EUR10/mes) y participe en seminarios web avanzados cerrados!
-
https://store.firebirdsql.org/p/firebird-associate-donation-eur/