Come gestire enormi set di risultati dal database

Sto progettando un’applicazione web basata su database multilivello: database relazionale SQL, Java per il livello di servizio intermedio, web per l’interfaccia utente. La lingua non ha molta importanza.

Il livello di servizio intermedio esegue l’interrogazione effettiva del database. L’interfaccia utente richiede semplicemente determinati dati e non ha alcun concetto che sia supportato da un database.

La domanda è come gestire grandi set di dati? L’interfaccia utente richiede dati, ma i risultati potrebbero essere enormi, forse troppo grandi per adattarsi alla memoria. Ad esempio, un’applicazione di un segnale stradale potrebbe avere un livello di servizio di:

StreetSign getStreetSign(int identifier) Collection getStreetSigns(Street street) Collection getStreetSigns(LatLonBox box) 

Il livello dell’interfaccia utente chiede di ottenere tutti i segnali stradali che soddisfano determinati criteri. A seconda dei criteri, il set di risultati potrebbe essere enorme. Il livello dell’interfaccia utente potrebbe dividere i risultati in pagine separate (per un browser) o semplicemente presentarli tutti (servendo fino a Goolge Earth). Il set di risultati potenzialmente enorme potrebbe essere un problema di prestazioni e risorse (memoria insufficiente).

Una soluzione è non restituire oggetti completamente caricati (oggetti StreetSign). Piuttosto, restituisci una sorta di set di risultati o iteratore che carica pigramente ogni singolo object.

Un’altra soluzione è modificare l’API del servizio per restituire un sottoinsieme dei dati richiesti:

 Collection getStreetSigns(LatLonBox box, int pageNumber, int resultsPerPage) 

Ovviamente l’interfaccia utente può ancora richiedere un enorme set di risultati:

 getStreetSigns(box, 1, 1000000000) 

Sono curioso di sapere qual è il modello standard di progettazione del settore per questo scenario?

La prima domanda dovrebbe essere:

¿L’utente deve o è in grado di gestire questa quantità di dati?

Sebbene il set di risultati debba essere impaginato, se la sua dimensione potenziale è così grande, la risposta sarà “probabilmente no”, quindi l’interfaccia utente non dovrebbe provare a mostrarla.

Ho lavorato a progetti J2EE su sistemi di assistenza sanitaria, che trattano enormi quantità di dati archiviati, letteralmente milioni di pazienti, visite, moduli, ecc. E la regola generale non è quella di mostrare più di 100 o 200 righe per qualsiasi ricerca utente, consigliando l’utente che tale insieme di criteri produce più informazioni che può comprendere.

Il modo di implementare questo varia da un progetto all’altro, è ansible forzare l’interfaccia utente a chiedere al livello di servizio la dimensione di una query prima di avviarla, oppure è ansible lanciare un’eccezione dal livello di servizio se il set di risultati cresce troppo (tuttavia in questo modo accoppia il livello di servizio con l’implementazione limitata di un’interfaccia utente).

Stai attento! Ciò non significa che ogni metodo sul livello di servizio deve generare un’eccezione se le sue dimensioni dei risultati sono superiori a 100, questa regola generale si applica solo ai set di risultati mostrati all’utente direttamente, che è un motivo migliore per posizionare il controllo nell’interfaccia utente invece sul livello di servizio.

Lo schema più frequente che ho visto per questa situazione è una sorta di paging, di solito fatto lato server per ridurre la quantità di informazioni inviate via cavo.

Ecco un esempio di SQL Server 2000 che utilizza una variabile di tabella (generalmente più veloce di una tabella temporanea) insieme all’esempio dei segnali stradali:

 CREATE PROCEDURE GetPagedStreetSigns ( @Page int = 1, @PageSize int = 10 ) AS SET NOCOUNT ON -- This memory-variable table will control paging DECLARE @TempTable TABLE (RowNumber int identity, StreetSignId int) INSERT INTO @TempTable ( StreetSignId ) SELECT [Id] FROM StreetSign ORDER BY [Id] -- select only those rows belonging to the requested page SELECT SS.* FROM StreetSign SS INNER JOIN @TempTable TT ON TT.StreetSignId = SS.[Id] WHERE TT.RowNumber BETWEEN ((@Page - 1) * @PageSize + 1) AND (@Page * @PageSize) 

In SQL Server 2005, è ansible ottenere più intelligenti con elementi quali Common Table Expressions e le nuove funzioni di classificazione SQL. Ma il tema generale è che si utilizza il server per restituire solo le informazioni che appartengono alla pagina corrente.

Tieni presente che questo approccio può diventare complicato se permetti all’utente finale di applicare filtri “al volo” ai dati che sta vedendo.

Direi che se il potenziale esiste per un grande insieme di dati, allora vai alla rotta di paging.

Puoi ancora impostare un MAX che non vuoi che vadano oltre.

EG SO usa dimensioni di pagina di 15, 30, 50 …

Una cosa di cui diffidare quando si lavora con classi di wrapper di riga cresciute come te (apparentemente), è il codice che rende ulteriori chiamate al database senza che tu (lo sviluppatore) ne sia consapevole. Ad esempio, potresti chiamare un metodo che restituisce una raccolta di oggetti Person e pensare che l’unica cosa che sta succedendo sotto il cofano sia una singola chiamata “SELECT * FROM PERSONS”. In realtà, il metodo che stai chiamando potrebbe scorrere l’insieme restituito di oggetti Person e effettuare ulteriori chiamate DB per popolare la raccolta di ogni Ordine di Person.

Come dici tu, una delle tue soluzioni è non restituire oggetti completamente caricati, quindi probabilmente sei a conoscenza di questo potenziale problema. Uno dei motivi per cui tendo ad evitare l’utilizzo dei wrapper di riga è che rendono inevitabilmente difficile mettere a punto l’applicazione e ridurre al minimo le dimensioni e la frequenza del traffico del database.

In ASP.NET userei il paging lato server, dove si recupera solo la pagina di dati che l’utente ha richiesto dall’archivio dati. Ciò si oppone al recupero dell’intero set di risultati, alla sua messa in memoria e alla sua ricerca su richiesta.

JSF o JavaServerFace dispone di widget per frammentare set di risultati di grandi dimensioni nel browser. Può essere parametrizzato come suggerisci. Non lo definirei un “modello standard di progettazione del settore” con qualsiasi mezzo, ma vale la pena dare un’occhiata a come qualcun altro ha risolto il problema.

Quando mi occupo di questo tipo di problema, di solito metto a pezzi i dati inviati al browser (o client thin / thick, a seconda di quale sia più appropriato per la tua situazione) a prescindere dalle dimensioni totali effettive dei dati che soddisfano determinati criteri, solo una piccola parte è davvero utilizzabile in qualsiasi interfaccia utente in una volta.

Vivo in un mondo Microsoft, quindi il mio ambiente principale è ASP.Net con SQL Server. Ecco due articoli sul paging (che menzionano alcune tecniche per sfogliare i set di risultati) che possono essere utili:

Ricerca in modo efficiente di molti dati (e in modo Ajax) con ASP.NET 2.0 Paging efficiente dei dati con il controllo DataList di ASP.NET 2.0 e ObjectDataSource

Un altro meccanismo che Microsoft ha spedito ultimamente è la loro idea di ” Dynamic Data “: potresti essere in grado di dare un’occhiata al coraggio di questo per alcune indicazioni su come hanno a che fare con questo problema.

Ho fatto cose simili su due prodotti diversi. In un caso l’origine dati è facoltativamente impaginata – per Java, implementa un’interfaccia Pagable simile a:

 public interface Pageable { public void setStartIndex( int index ); public int getStartIndex(); public int getRowsPerPage() throws Exception; public void setRowsPerPage( int rowsPerPage ); } 

L’origine dati implementa un altro metodo per get () di elementi e l’implementazione di un’origine dati impaginata restituisce semplicemente la pagina corrente. Quindi puoi impostare il tuo indice di partenza e prendere una pagina nel tuo controller.

Una cosa da considerare sarà quella di memorizzare nella cache il lato server dei cursori. Per un’app Web dovrai scadere, ma saranno davvero utili per le prestazioni.

Il progetto del repository digitale di fedora restituisce il numero massimo di risultati con un ID set-set. Quindi ottieni il resto del risultato chiedendo il prossimo pezzo che fornisce l’ID risultato-risultato nella query successiva. Funziona bene fino a quando non si desidera eseguire alcuna ricerca o ordinamento al di fuori della query.

Dal livello di recupero dei dati, lo schema di progettazione standard prevede due interfacce di metodo, una per tutte e una per una dimensione di blocco.

Se lo desideri, puoi sovrapporre componenti che eseguono il paging su di esso.