15 antipattern di Firebird
by Alexey Kovyazin, 14-gen-2025
Introduzione
Questo documento illustra 15 anti-pattern comuni quando si lavora con database Firebird e fornisce soluzioni per ciascuno di essi.
1. Query parallele multiple su MON$
Anti-pattern: Un errore molto diffuso - trigger OnConnect, query su MON$ATTACHMENTS per selezionare i dettagli dell’utente per scopi di audit, o calcolare il numero di connessioni per scopi di licenza.
Perché è negativo?
-
Le tabelle MON$ sono tabelle virtuali memorizzate nei file di sistema fbNN_mon_xx, con statistiche sulle prestazioni, ecc.
-
File >1Gb significa che lo stai usando troppo
-
Sono progettate esclusivamente per l’uso da parte degli amministratori di sistema - cioè, 1-2 query parallele, esclusivamente per gli amministratori
-
200+ connessioni con query parallele su MON$ rallenteranno Firebird in modo molto significativo, e 500+ query simultanee “bloccheranno” Firebird con alte probabilità
Soluzioni:
-
Non usare MON$ per attività non amministrative, cioè per contare o fare audit, evitare di usarle in OnConnect
-
Per scopi di audit:
-
Usare variabili di contesto come CURRENT_USER, CURRENT_TIMESTAMP, ecc.
-
Usare Audit - funzionalità nativa di Firebird, molto più potente dei trigger
-
Per scopi di licenza - usare le variabili di contesto dell’utente
2. Caricamento Lento della Dashboard
Anti-pattern: Caricare dashboard o scoreboard complete che sommano tutti gli ordini e le fatture dell’ultimo mese o anno all’avvio dell’applicazione, o aggiornare alcune metriche ogni minuto o più spesso.
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';
Perché è negativo?
-
Gli utenti devono attendere diversi secondi per vedere le statistiche a livello aziendale prima di poter iniziare il loro lavoro effettivo
-
Dal punto di vista di Firebird - per eseguire costantemente molte query parallele, recuperare grandi quantità di dati, ordinarli/raggrupparli, Firebird utilizzerà intensamente più core della CPU, leggendo dal disco, dalla cache, dalla memoria dedicata all’ordinamento (e a volte l’ordinamento va su disco)
-
È come costruire un report diverse volte al minuto!
Soluzioni:
- Ridurre il numero di utenti che vedranno le dashboard:
-
Di solito la Dashboard è necessaria solo per analisti e management, escluderla dal caricamento generale dell’applicazione
-
Rendere il caricamento della dashboard all’avvio/per qualche modulo opzionale, disabilitato per impostazione predefinita
-
Caricare i dati della dashboard tramite clic esplicito su un pulsante, non all’avvio (cioè, trattarla come un report)
-
Calcolare i dati della dashboard con 1 processo secondo una pianificazione (cioè, un robot) e memorizzarli in una tabella semplice pronta per essere recuperata con una query semplice
-
Usare trigger per aggregare i dati e memorizzarli pronti per l’uso
-
Usare un database replica per calcolare i dati delle dashboard (e anche tutti i report pesanti)
3. Caricamento di Record Non Necessari
Anti-pattern: Caricare tutti i dati senza filtri nella griglia all’apertura di un’applicazione o di un modulo, indipendentemente dal fatto che contenga centinaia di migliaia di record.
procedure TDataForm.LoadAllRecords;
begin
FDQuery1.SQL.Text := 'SELECT * FROM large_table';
FDQuery1.Open;
// Carica l'intera tabella in memoria
DBGrid1.DataSource.DataSet := FDQuery1;
end;
Perché è negativo?
-
Nonostante la griglia mostri solo 50 record, gli utenti devono scorrere migliaia di record invece di usare la funzionalità di ricerca
-
Nel 99% dei casi gli utenti hanno bisogno di un sottoinsieme molto ristretto di dati: i record di vendita più recenti, per esempio
-
Dal punto di vista di Firebird:
-
Ogni apertura richiede lettura, memorizzazione in cache e trasferimento di migliaia di record attraverso la rete
-
Se mantieni il dataset aperto (in Delphi), Firebird mantiene buffer, record ordinati nello spazio temporaneo (se ORDER BY, GROUP BY, ecc.) fino alla chiusura del dataset
Soluzioni:
-
Limitare il numero di record con FIRST/SKIP/ROWS
-
Limitare il numero di record con alcuni criteri, per esempio, mostrare i record creati/modificati negli ultimi 3 giorni
-
In generale, chiudere le query il prima possibile.
4. Query Eccessive sullo Scorrimento
Anti-pattern: Eseguire query sugli eventi di scorrimento. Per esempio, quando si visualizzano dati in una griglia o tabella, eseguire una query separata PER OGNI record, o se stai usando l’esempio classico di scorrimento master-detail in 2 griglie senza ritardo.
procedure TForm1.GridScrolled(Sender: TObject);
begin
// query per ogni riga
FDQuery2.SQL.Text :=
'SELECT additional_info FROM details ' +
'WHERE id = ' + IntToStr(CurrentRowId);
FDQuery2.Open;
end;
Perché è negativo?
-
Eseguire una query separata PER OGNI record in una griglia dinamica costringe Firebird a elaborare migliaia di piccole query, consumando inutilmente risorse della CPU
-
Dal punto di vista di Firebird:
-
Molte (migliaia al secondo) piccole query creeranno un carico significativo sulla CPU, perché anche se la query mostra 0ms nelle statistiche, richiede preparazione, esecuzione, trasferimento del risultato, ecc.
Soluzioni:
-
Caricare più righe contemporaneamente usando operazioni batch
-
Migliorare la query principale per la griglia per eseguire la query dettagliata come parte di essa
-
Aggiungere un pulsante esplicito per caricare i dettagli per la parte visibile della griglia
-
Aggiungere un ritardo per eseguire la query per ricevere i dettagli, per prevenire query immediate durante lo scorrimento
-
Non abilitare il caricamento dei dettagli durante lo scorrimento per tutti gli utenti per impostazione predefinita
5. Aggiornamenti Automatici Non Necessari
Anti-pattern: Aggiornare automaticamente i dati della griglia a intervalli minimi in ogni applicazione client, con questa funzionalità abilitata per impostazione predefinita.
Perché è negativo?
-
Questo comporta centinaia di connessioni client che eseguono query quasi identiche per recuperare gli stessi record
-
Dove accade: aggiornamenti automatici per pianificazioni, o selezioni per posizioni in coda, o ricerca dello “slot più vicino”, ecc.
-
Dal punto di vista di Firebird:
-
Combinazione di caricamento di dashboard ed eventi di scorrimento: molte query di medie dimensioni creano carico sul sistema
Soluzioni:
-
Aumentare l’intervallo!
-
Implementare aggiornamenti espliciti (attivati dall’utente)
-
Usare aggiornamenti selettivi del dataset basati su modifiche effettive dei dati (streaming o trigger o evento+streaming)
6. Aggiornamenti Frequenti dei Record
Anti-pattern: Aggiornare frequentemente lo stesso record in transazioni diverse, creando numerose versioni del record.
Perché è negativo?
-
Un record con dozzine di versioni può degradare significativamente le prestazioni, un record con migliaia può diventare un blocco
-
Dal punto di vista di Firebird: la catena delle versioni del record deve essere ricostruita per identificare la versione corretta per la transazione specifica, richiede numerose operazioni di lettura, e di conseguenza, la garbage collection diventa significativamente più lenta.
Soluzioni:
-
Migrare a Firebird 4+, c’è la garbage collection intermedia
-
Non mantenere transazioni scrivibili di lunga durata, eseguire una corretta garbage collection
-
Per Firebird <4, considerare l’uso di DELETE+INSERT invece di UPDATE
7. Uso di transazioni di scrittura per selezioni di sola lettura
Anti-pattern: Usare transazioni di scrittura per selezioni di sola lettura porta a operazioni eccessive.
Perché è negativo?
-
Usare transazioni di scrittura per selezioni di sola lettura porta a molte scritture non necessarie delle pagine di intestazione
-
Usare transazioni di scrittura per operazioni di sola lettura è inefficiente (grande TIP durante il commit crea carico aggiuntivo sul server)
Soluzioni:
-
Usare una transazione separata di sola lettura per le operazioni che non modificano i dati
-
Firebird è uno dei pochi database che permette di aprire più transazioni nell’ambito di una singola connessione
-
Le tabelle temporanee globali sono disponibili per l’uso in transazioni di sola lettura
8. Uso di LIKE :param
La seguente query con parametro non userà l’indice per il campo (anche se l’indice esiste):
SELECT * FROM Table1 WHERE fieldName LIKE :param1
Perché è negativo?
Poiché LIKE consente la ricerca con caratteri jolly (%), che possono sostituire qualsiasi numero di simboli, Firebird non può determinare in anticipo se il valore del parametro sarà adatto per la ricerca con indice.
Di solito gli sviluppatori cercano di aggirare il problema incorporando il valore del parametro nel testo della query:
-
fieldName LIKE «Alex%» - possibile usare l’indice
-
fieldName LIKE «%Alex» - non possibile usare l’indice standard
-
fieldName LIKE «%Alex%» - non possibile usare l’indice affatto
Questo porta ad altri problemi (vedi #10 sotto).
Soluzioni:
1. Usare STARTING WITH per prefissi di stringa noti
Quando il tuo valore di ricerca non inizia mai con un carattere jolly %, preferisci STARTING WITH invece di LIKE:
WHERE fieldName STARTING WITH ?param1
2. Ottimizzare le ricerche di stringhe bidirezionali
Per stringhe con schemi di prefisso o suffisso noti, usa un indice invertito:
-- Crea indice invertito
CREATE INDEX ixreverse1 ON TABLE1 COMPUTED BY (REVERSE(fieldName));
-- Query che usa entrambe le direzioni
WHERE fieldName STARTING WITH :param1
OR reverse(fieldName) STARTING WITH reverse(:param2)
3. Implementare una strategia di ricerca progressiva
Per stringhe che appaiono all’inizio/fine/centro (ma non simultaneamente):
-
Prima prova una ricerca veloce con indice usando STARTING WITH
-
Se non vengono trovati risultati, ripiega su una ricerca LIKE più lenta
4. Ottimizzazione della Ricerca Basata su Parole
Quando si cercano parole complete (delimitate da spazi, virgole, ecc.):
-
Creare una tabella separata di mappatura parola-ID
-
Cercare attraverso la tabella di mappatura invece del testo originale
5. Per funzionalità complete di ricerca full-text:
-
Considerare l’uso di IBSurgeon Full Text Search UDR
-
Questa soluzione open-source fornisce funzionalità avanzate di ricerca testuale
9. Non chiudere le transazioni per operazioni di sola lettura
Perché è negativo?
- Mantenere le transazioni aperte per periodi prolungati potrebbe costringere Firebird a mantenere numerose versioni precedenti per potenziali transazioni snapshot
Soluzioni:
-
Usare transazioni di sola lettura dove possibile, e chiudere le transazioni scrivibili il prima possibile
-
Usare versioni moderne di Firebird (4+) per ridurre l’impatto delle catene di versioni dei record
-
Implementare uno sweep corretto
10. Problemi di Parametrizzazione delle Query
Anti-pattern: Evitare query preparate e parametrizzazione, incorporando invece i valori dei parametri direttamente nel testo della query.
FDQuery1.SQL.Text :=
'SELECT * FROM users WHERE name = ''' +
EditUsername.Text + '''';
FDQuery1.Open;
Perché è negativo?
-
Questa pratica riduce le prestazioni per query ripetute
-
Ogni query con valori di parametri incorporati deve essere preparata come nuova
-
La preparazione può essere lunga e dispendiosa per tabelle grandi
-
Complica l’analisi dei problemi
-
È difficile raggruppare le query per testo
-
Crea vulnerabilità di SQL injection
Soluzioni:
FDQuery1.SQL.Text :=
'SELECT * FROM users WHERE name = :username';
FDQuery1.ParamByName('username').AsString :=
EditUsername.Text;
FDQuery1.Open;
11. Controllo di Integrità Errato: trigger/CHECK invece della Chiave Primaria
Anti-pattern: Usare trigger o CHECK invece delle Chiavi Primarie per i controlli di integrità del database.
Perché è negativo?
-
Questo ignora che la validazione della Chiave Primaria usa la modalità speciale per leggere la versione corrente del record, indipendentemente dal livello di isolamento della transazione dell’utente.
-
Fare controlli di PK con trigger nelle transazioni utente aumenta la possibilità di duplicazione e complica inutilmente la logica
Soluzioni:
-
Usare chiavi primarie
-
Evitare controlli di integrità ridondanti
-
Mantenere la logica del database semplice
12. Generazione di ID con MAX()
Anti-pattern: Usare MAX(id)+1 per nuovi identificatori è inaffidabile e inefficiente.
INSERT INTO users (id, name)
VALUES ((SELECT MAX(id) + 1 FROM users),
'John Doe');
Perché è negativo?
-
Usare MAX(id)+1 invece di sequenze (generatori) per nuovi identificatori
-
MAX(id)+1 non garantisce l’unicità con parametri di transazione comuni - due transazioni parallele potrebbero ricevere lo stesso valore MAX()
-
La combinazione di Max()+1 e CHECK(select if unique) non funziona nemmeno!
Soluzioni:
-- Usa generatore/sequenza!
CREATE GENERATOR gen_user_id;
-- Usa il generatore per la generazione dell'ID
INSERT INTO users (id, name)
VALUES (
GEN_ID(gen_user_id, 1),
'John Doe' );
## 13. Uso inefficiente dei GUID
**Perché è un problema?**
- L'uso di GUID generati dal sistema invece di gen\_uuid() può influire sulle prestazioni degli indici
- I GUID generati dal sistema sono altamente casuali
**Soluzioni:**
- Utilizzare la funzione gen\_uuid()
- Considerare l'uso di BIGINT
- Nella versione 6 ci sarà UUID v7
## 14. Campi calcolati inefficienti
**Anti-pattern:** L'uso di campi calcolati con SELECT su altre tabelle riduce significativamente le prestazioni delle semplici operazioni SELECT.
```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)));
Perché è un problema?
-
I campi calcolati vengono calcolati al volo e non sono pensati per implementare logiche complesse, e possono complicare notevolmente gli sforzi di ottimizzazione
-
Rafforza le relazioni tra le tabelle
-
Ha senso utilizzare i campi calcolati solo per calcoli leggeri con i campi della tabella, come la concatenazione
Soluzioni:
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. Soppressione degli errori senza registrazione
Anti-pattern: Non sopprimere errori e avvisi di Firebird senza registrarli!
try
FDQuery1.Open;
except
// Errore silenzioso
end;
Perché è un problema?
- Nascondere gli errori impedisce una corretta diagnosi e il debug. Una corretta registrazione degli errori è fondamentale per comprendere e risolvere rapidamente i problemi.
Soluzioni:
try
FDQuery1.Open;
except
on E: Exception do
begin
// Registrazione completa
Logger.Error('Connessione al database fallita: ' + E.Message);
ShowMessage('Impossibile connettersi al database. Contattare il supporto.');
// Registra contesto aggiuntivo
Logger.LogStackTrace(E);
end;
end;
Informazioni di contatto
-
Invia le tue domande a [email protected]
-
Diventa Firebird Supporter (da EUR10/mese) e partecipa a webinar avanzati riservati!
-
https://store.firebirdsql.org/p/firebird-associate-donation-eur/