Questa pagina è stata tradotta automaticamente. Leggi l'originale in inglese. English

Libreria IBSurgeon

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.

sql
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:

  1. 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)

  1. 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

  2. Usare trigger per aggregare i dati e memorizzarli pronti per l’uso

  3. 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.

delphi
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:

  1. Limitare il numero di record con FIRST/SKIP/ROWS

  2. Limitare il numero di record con alcuni criteri, per esempio, mostrare i record creati/modificati negli ultimi 3 giorni

  3. 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.

delphi
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:

  1. Caricare più righe contemporaneamente usando operazioni batch

  2. Migliorare la query principale per la griglia per eseguire la query dettagliata come parte di essa

  3. Aggiungere un pulsante esplicito per caricare i dettagli per la parte visibile della griglia

  4. Aggiungere un ritardo per eseguire la query per ricevere i dettagli, per prevenire query immediate durante lo scorrimento

  5. 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:

  1. Aumentare l’intervallo!

  2. Implementare aggiornamenti espliciti (attivati dall’utente)

  3. 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:

  1. Migrare a Firebird 4+, c’è la garbage collection intermedia

  2. Non mantenere transazioni scrivibili di lunga durata, eseguire una corretta garbage collection

  3. 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):

sql
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:

sql
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:

sql
-- 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.

delphi
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:

delphi
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.

sql
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:

sql
-- 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:

sql
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!

delphi
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:

delphi
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