Come archiviare le informazioni ordinate in un database relazionale


20

Sto cercando di capire come archiviare correttamente le informazioni ordinate in un database relazionale.

Un esempio:

Di 'che ho una playlist, composta da canzoni. All'interno del mio database relazionale, ho una tabella Playlistscontenente alcuni metadati (nome, creatore, ecc.). Ho anche una tabella chiamata Songs, contenente un playlist_id, nonché informazioni specifiche della canzone (nome, artista, durata, ecc.).

Per impostazione predefinita, quando una nuova canzone viene aggiunta a una playlist, viene aggiunta alla fine. Quando si ordina su Song-ID (crescente), l'ordine sarà l'ordine di aggiunta. E se un utente dovesse essere in grado di riordinare i brani nella playlist?

Ho avuto un paio di idee, ognuna con i suoi vantaggi e svantaggi:

  1. Una colonna chiamata order, che è un numero intero . Quando una canzone viene spostata, l'ordine di tutte le canzoni tra la sua vecchia e la nuova posizione viene modificato, per riflettere il cambiamento. Lo svantaggio di questo è che molte domande devono essere fatte ogni volta che una canzone viene spostata e l'algoritmo mobile non è così banale come con le altre opzioni.
  2. Una colonna chiamata order, che è un decimale ( NUMERIC). Quando viene spostato un brano, viene assegnato il valore in virgola mobile tra i due numeri adiacenti. Svantaggio: i campi decimali occupano più spazio e potrebbe essere possibile rimanere senza precisione, a meno che non si prenda cura di ridistribuire l'intervallo dopo alcune modifiche.
  3. Un altro modo sarebbe quello di avere un previouse un nextcampo che faccia riferimento ad altre canzoni. (o sono NULL nel caso del primo, rispettivamente dell'ultimo brano della playlist in questo momento; in pratica crei un elenco collegato ). Svantaggio: query come "trova la Xth Song nell'elenco" non sono più a tempo costante, ma a tempo lineare.

Quale di queste procedure viene spesso utilizzata nella pratica? Quale di queste procedure è più veloce su database medio-grandi? Ci sono altri modi per archiviarlo?

EDIT: Per semplicità, nell'esempio una Song appartiene solo a una Playlist (una relazione molti-a-uno). Ovviamente, si potrebbe anche usare una tabella di giunzione, quindi la playlist di una canzone è una relazione molti-a-molti (e applicare una delle strategie sopra descritte su quella tabella).


1
È possibile utilizzare l'opzione 1 (ordine come intero) con 100 passaggi. Quindi non è necessario riordinare se si sposta un brano, basta prendere un valore compreso tra 100. Di tanto in tanto potrebbe essere necessario un nuovo rinumerazione per ottenere nuovamente spazi vuoti tra i brani.
annodare l'

4
"Lo svantaggio di questo è che molte domande devono essere fatte ogni volta che una canzone viene spostata"?! - update songorder set order = order - 1 where order >= 12 & order <= 42; update songorder set order = 42 where id = 123;- Sono due aggiornamenti, non trenta. Tre se si desidera mettere in ordine un vincolo univoco.

2
Usa l'opzione 1 a meno che tu non sappia per certo che hai bisogno di qualcos'altro. Un problema per i programmatori che non conoscono i database non è capire che i database sono molto, molto bravi in ​​questo genere di cose. Non aver paura di mettere al lavoro il tuo db.
GrandmasterB,

1
Queries like 'find the Xth Song in the list' are no longer constant-timevale anche per l'opzione 2.
Doc Brown,

2
@MikeNakis: sembra costoso, ma tutto il lavoro viene svolto sul server, che è (di solito) ottimizzato per questo tipo di lavoro. Non userei questa tecnica su una tabella con milioni di righe, ma non la sconto per una tabella con solo un paio di migliaia.
TMN,

Risposte:


29

I database sono ottimizzati per alcune cose. L'aggiornamento rapido di molte righe è uno di questi. Ciò diventa particolarmente vero quando si lascia che il database faccia il suo lavoro.

Ritenere:

order song
1     Happy Birthday
2     Beat It
3     Never Gonna Give You Up
4     Safety Dance
5     Imperial March

E vuoi passare Beat Italla fine, avresti due domande:

update table 
  set order = order - 1
  where order >= 2 and order <= 5;

update table
  set order = 5
  where song = 'Beat It'

E questo è tutto. Questo si espande molto bene con numeri molto grandi. Prova a mettere qualche migliaio di brani in una playlist ipotetica nel tuo database e vedi quanto tempo ci vuole per spostare un brano da una posizione all'altra. Poiché questi hanno forme molto standardizzate:

update table 
  set order = order - 1
  where order >= ? and order <= ?;

update table
  set order = ?
  where song = ?

Hai due dichiarazioni preparate che puoi riutilizzare in modo molto efficiente.

Ciò offre alcuni vantaggi significativi: l'ordine della tabella è qualcosa su cui puoi ragionare. La terza canzone ha un order3, sempre. L'unico modo per garantire ciò è utilizzare numeri interi consecutivi come ordine. L'uso di elenchi pseudo-collegati o numeri decimali o numeri interi con spazi vuoti non ti garantirà questa proprietà; in questi casi l'unico modo per ottenere l'ennesimo brano è ordinare l'intero tavolo e ottenere l'ennesimo disco.

E davvero, questo è molto più facile di quanto pensi. È semplice capire cosa si desidera fare, generare le due dichiarazioni di aggiornamento e far sì che altre persone guardino quelle due dichiarazioni di aggiornamento e si rendano conto di ciò che viene fatto.


2
Sto iniziando ad apprezzare questo approccio.
Mike Nakis,

2
@MikeNakis funziona bene. C'è anche un albero binario che si basa su un'idea simile: l' albero preordinato modificato . Ci vuole un po 'di più per capovolgere, ma ti permette di fare delle domande molto carine per i dati gerarchici. Non ho mai avuto problemi di prestazioni, anche su alberi di grandi dimensioni. Essere in grado di ragionare sul codice è qualcosa su cui ho posto grande enfasi fino a quando non viene dimostrato che il codice semplice manca delle prestazioni necessarie (e che è stato solo in situazioni estreme).

Ci saranno problemi con l'utilizzo orderpoiché order byè una parola chiave?
kojow7,

@ kojow7, se i tuoi campi hanno nomi in conflitto con le parole chiave, dovresti racchiuderli tra i segni di spunta "` ".
Andri,

Questo approccio ha senso, ma qual è il modo migliore per ottenere il ordervalore quando si aggiunge un nuovo brano a una playlist. Supponiamo che sia la nona canzone, c'è un modo migliore per inserire 9 orderrispetto a fare un COUNTprecedente prima di aggiungere il disco?
delashum,

3

Prima di tutto, non è chiaro dalla descrizione di ciò che hai fatto, ma hai bisogno di una PlaylistSongstabella che contenga a PlaylistIde a SongId, che descriva quali canzoni appartengono a quali playlist.

È in questa tabella che è necessario aggiungere le informazioni di ordinazione.

Il mio meccanismo preferito è con numeri reali. L'ho implementato di recente e ha funzionato come un fascino. Quando si desidera spostare un brano in una posizione specifica, si calcola il suo nuovo Orderingvalore come media dei Orderingvalori del brano precedente e del brano successivo. Se si utilizza un numero reale a 64 bit, si esaurirà la precisione all'incirca nello stesso momento in cui l'inferno si bloccherà, ma se si sta davvero scrivendo il proprio software per i posteri, quindi considerare di riassegnare dei bei Orderingvalori interi arrotondati a tutti i brani in ciascuno playlist ogni tanto.

Come bonus aggiuntivo, ecco il codice che ho scritto che lo implementa. Ovviamente non puoi usarlo così com'è, e sarebbe troppo lavoro per me in questo momento disinfettarlo per te, quindi lo sto solo postando per te per trarne idee.

La classe è ParameterTemplate(qualunque cosa, non chiedere!) Il metodo ottiene l'elenco dei modelli di parametri a cui questo modello appartiene dal suo genitore ActivityTemplate. (Qualunque cosa, non chiedere!) Il codice contiene un po 'di protezione contro la mancanza di precisione. Il divisore viene utilizzato per i test: il test unitario utilizza un divisore di grandi dimensioni in modo da esaurire rapidamente la precisione e quindi attivare il codice di protezione di precisione. Il secondo metodo è pubblico e "solo per uso interno; non invocare" in modo che il codice di test possa invocarlo. (Non potrebbe essere un pacchetto privato perché il mio codice di test non si trova nello stesso pacchetto del codice che verifica.) Il campo che controlla l'ordinamento viene chiamatoOrdering , accessibile tramite getOrdering()e setOrdering(). Non vedi alcun SQL perché sto usando la mappatura relazionale degli oggetti tramite Hibernate.

/**
 * Moves this {@link ParameterTemplate} to the given index in the list of {@link ParameterTemplate}s of the parent {@link ActivityTemplate}.
 *
 * The index must be greater than or equal to zero, and less than or equal to the number of entries in the list.  Specifying an index of zero will move this item to the top of
 * the list. Specifying an index which is equal to the number of entries will move this item to the end of the list.  Any other index will move this item to the position
 * specified, also moving other items in the list as necessary. The given index cannot be equal to the current index of the item, nor can it be equal to the current index plus
 * one.  If the given index is below the current index of the item, then the item will be moved so that its new index will be equal to the given index.  If the given index is
 * above the current index, then the new index of the item will be the given index minus one.
 *
 * NOTE: this method flushes the persistor and refreshes the parent node so as to guarantee that the changes will be immediately visible in the list of {@link
 * ParameterTemplate}s of the parent {@link ActivityTemplate}.
 *
 * @param toIndex the desired new index of this {@link ParameterTemplate} in the list of {@link ParameterTemplate}s of the parent {@link ActivityTemplate}.
 */
public void moveAt( int toIndex )
{
    moveAt( toIndex, 2.0 );
}

/**
 * For internal use only; do not invoke.
 */
public boolean moveAt( int toIndex, double divisor )
{
    MutableList<ParameterTemplate<?>> parameterTemplates = getLogicDomain().getMutableCollections().newArrayList();
    parameterTemplates.addAll( getParentActivityTemplate().getParameterTemplates() );
    assert parameterTemplates.getLength() >= 1; //guaranteed since at the very least, this parameter template must be in the list.
    int fromIndex = parameterTemplates.indexOf( this );
    assert 0 <= toIndex;
    assert toIndex <= parameterTemplates.getLength();
    assert 0 <= fromIndex;
    assert fromIndex < parameterTemplates.getLength();
    assert fromIndex != toIndex;
    assert fromIndex != toIndex - 1;

    double order;
    if( toIndex == 0 )
    {
        order = parameterTemplates.fetchFirstElement().getOrdering() - 1.0;
    }
    else if( toIndex == parameterTemplates.getLength() )
    {
        order = parameterTemplates.fetchLastElement().getOrdering() + 1.0;
    }
    else
    {
        double prevOrder = parameterTemplates.get( toIndex - 1 ).getOrdering();
        parameterTemplates.moveAt( fromIndex, toIndex );
        double nextOrder = parameterTemplates.get( toIndex + (toIndex > fromIndex ? 0 : 1) ).getOrdering();
        assert prevOrder <= nextOrder;
        order = (prevOrder + nextOrder) / divisor;
        if( order <= prevOrder || order >= nextOrder ) //if the accuracy of the double has been exceeded
        {
            parameterTemplates.clear();
            parameterTemplates.addAll( getParentActivityTemplate().getParameterTemplates() );
            for( int i = 0; i < parameterTemplates.getLength(); i++ )
                parameterTemplates.get( i ).setOrdering( i * 1.0 );
            rocs3dDomain.getPersistor().flush();
            rocs3dDomain.getPersistor().refresh( getParentActivityTemplate() );
            moveAt( toIndex );
            return true;
        }
    }
    setOrdering( order );
    rocs3dDomain.getPersistor().flush();
    rocs3dDomain.getPersistor().refresh( getParentActivityTemplate() );
    assert getParentActivityTemplate().getParameterTemplates().indexOf( this ) == (toIndex > fromIndex ? toIndex - 1 : toIndex);
    return false;
}

Userei un ordinamento intero e se pensassi che il riordino fosse troppo costoso, ridurrei semplicemente il numero di riordini, facendo saltare ciascuno di X di X, dove X è la quantità di cui ho bisogno per ridurre il riordino di, diciamo 20, che dovrebbe andare bene come antipasto.
Warren P

1
@WarrenP sì, lo so, può anche essere fatto in questo modo, ecco perché ho appena chiamato questo approccio "il mio preferito" anziché "il migliore" o "l'unico".
Mike Nakis,

0

Ciò che ha funzionato per me, per un piccolo elenco dell'ordine di 100 articoli è stato quello di adottare un approccio ibrido:

  1. Decimal SortOrder, ma con sufficiente precisione per memorizzare 0,5 differenze (ovvero Decimal (8,2) o qualcosa del genere).
  2. Durante l'ordinamento, afferrare i PK della riga sopra e sotto dove è stata spostata la riga corrente, se esistono. (Non avrai una riga sopra se sposti l'elemento nella prima posizione, per esempio)
  3. Registrare i PK della riga corrente, precedente e successiva sul server per eseguire l'ordinamento.
  4. Se hai una riga precedente, imposta la posizione della riga corrente su precedente + 0,5. Se hai solo un prossimo, imposta la posizione della riga corrente su successivo - 0,5.
  5. Successivamente ho un Proc memorizzato che aggiorna tutte le posizioni usando la funzione Row_Number di SQL Server, ordinando secondo il nuovo ordinamento. Questo trasformerà l'ordinamento da 1,1.5,2,3,4,6 a 1,2,3,4,5,6, poiché la funzione row_number fornisce ordinali interi.

Quindi si finisce con un ordine intero senza spazi vuoti, memorizzato in una colonna decimale. È abbastanza pulito, mi sento. Ma potrebbe non scalare molto bene una volta che hai centinaia di migliaia di righe che devi aggiornare, tutte in una volta. Ma se lo fai, perché stai usando un ordinamento definito dall'utente in primo luogo? (Nota: se si dispone di una tabella di grandi dimensioni con milioni di utenti ma ogni utente ha solo poche centinaia di elementi da ordinare, è possibile utilizzare l'approccio sopra semplicemente bene poiché si utilizzerà comunque una clausola where per limitare le modifiche a un solo utente )

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.