ORDER BY e confronto di stringhe miste di lettere e numeri


9

Dobbiamo fare dei resoconti su valori che di solito sono stringhe miste di numeri e lettere che devono essere ordinati 'naturalmente'. Ad esempio "P7B18" o "P12B3". @Le stringhe saranno principalmente sequenze di lettere quindi numeri alternati. Tuttavia, il numero di questi segmenti e la lunghezza di ciascuno possono variare.

Vorremmo che le parti numeriche di queste fossero ordinate in ordine numerico. Ovviamente, se gestisco quei valori di stringa direttamente con ORDER BY, allora "P12B3" verrà prima di "P7B18", poiché "P1" è precedente a "P7", ma mi piacerebbe il contrario, poiché "P7" naturalmente precede "P12".

Mi piacerebbe anche essere in grado di fare confronti di portata, ad esempio @bin < 'P13S6'o alcuni di questi. Non devo gestire i numeri in virgola mobile o negativi; questi saranno rigorosamente numeri interi non negativi con cui abbiamo a che fare. Le lunghezze delle stringhe e il numero di segmenti potrebbero essere potenzialmente arbitrari, senza limiti superiori fissi.

Nel nostro caso, l'involucro delle stringhe non è importante, anche se se c'è un modo per farlo in modo attento alle regole di confronto, altri potrebbero trovarlo utile. La parte più brutta di tutto ciò è che mi piacerebbe essere in grado di eseguire sia l'ordinamento sia il filtraggio di intervallo nella WHEREclausola.

Se lo facessi in C #, sarebbe un compito piuttosto semplice: fare un po 'di analisi per separare l'alfa dal numerico, implementare IComparable e il gioco è fatto. SQL Server, ovviamente, non sembra offrire alcuna funzionalità simile, almeno per quanto ne so.

Qualcuno sa qualche buon trucco per farlo funzionare? Esiste una capacità poco pubblicizzata di creare tipi CLR personalizzati che implementano IComparable e si comportano come previsto? Inoltre, non sono contrario ai trucchi XML stupidi (vedi anche: concatenazione di elenchi) e ho anche funzioni di wrapper di corrispondenza / estrazione / sostituzione regex CLR disponibili sul server.

EDIT: Come esempio leggermente più dettagliato, vorrei che i dati si comportassero in questo modo.

SELECT bin FROM bins ORDER BY bin

bin
--------------------
M7R16L
P8RF6JJ
P16B5
PR7S19
PR7S19L
S2F3
S12F0

cioè suddividere le stringhe in token di tutte le lettere o di tutti i numeri e ordinarle rispettivamente in ordine alfabetico o numerico, con i token più a sinistra che rappresentano il termine di ordinamento più significativo. Come ho già detto, un gioco da ragazzi in .NET se si implementa IComparable, ma non so come (o se) è possibile fare questo tipo di cose in SQL Server. Non è certo qualcosa in cui mi sia mai imbattuto in circa 10 anni di lavoro.


Potresti farlo con una specie di colonna calcolata indicizzata, trasformando la stringa in un numero intero. Così P7B12potrebbe diventare P 07 B 12, quindi (tramite ASCII) 80 07 65 12, quindi80076512
Philᵀᴹ

Ti suggerisco di creare una colonna calcolata che pad ogni componente numerico di una grande lunghezza (cioè 10 zeri). Poiché il formato è piuttosto arbitrario, avrai bisogno di un'espressione in linea piuttosto grande, ma è fattibile. Quindi puoi indicizzare / ordinare per / dove su quella colonna quanto vuoi.
Nick.McDermaid,

Si prega di vedere il link che ho appena aggiunto all'inizio della mia risposta :)
Solomon Rutzky

1
@srutzky Nice, ho votato a favore.
db2,

Ehi db2: a causa del passaggio di Microsoft da Connect a UserVoice e di non tenere esattamente il conteggio dei voti (lo inseriscono in un commento ma non sono sicuri che lo guardino), potrebbe essere necessario votare nuovamente: Supporta "ordinamento naturale" / DIGITSASNUMBERS come opzione di confronto . Grazie!
Solomon Rutzky,

Risposte:


8

Desideri un mezzo ragionevole ed efficiente per ordinare i numeri nelle stringhe come numeri reali? Prendi in considerazione la possibilità di votare il mio suggerimento per Microsoft Connect: supporta "ordinamento naturale" / DIGITSASNUMBERS come opzione di confronto


Non esiste un modo semplice e integrato per farlo, ma qui c'è una possibilità:

Normalizza le stringhe riformattandole in segmenti di lunghezza fissa:

  • Crea una colonna di ordinamento di tipo VARCHAR(50) COLLATE Latin1_General_100_BIN2. Potrebbe essere necessario regolare la lunghezza massima di 50 in base al numero massimo di segmenti e alle loro potenziali lunghezze massime.
  • Mentre la normalizzazione potrebbe essere eseguita nel livello dell'app in modo più efficiente, gestirla nel database utilizzando un UDF T-SQL consentirebbe di posizionare l'UDF scalare in un AFTER [or FOR] INSERT, UPDATEtrigger in modo tale da garantire la corretta impostazione del valore per tutti i record, anche quelli arrivando tramite query ad hoc, ecc. Naturalmente, anche l'UDF scalare può essere gestito tramite SQLCLR, ma dovrebbe essere testato per determinare quale fosse effettivamente più efficiente. **
  • L'UDF (indipendentemente dall'essere in T-SQL o SQLCLR) dovrebbe:
    • Elabora un numero sconosciuto di segmenti leggendo ciascun carattere e fermandosi quando il tipo passa da alfa a numerico o da numerico a alfa.
    • Per ogni segmento dovrebbe restituire una stringa di lunghezza fissa impostata sui caratteri / cifre massimi possibili di qualsiasi segmento (o forse max + 1 o 2 per tenere conto della crescita futura).
    • I segmenti alfa devono essere giustificati a sinistra e riempiti a destra con spazi.
    • I segmenti numerici devono essere giustificati a destra e riempiti a sinistra di zero.
    • Se i caratteri alfa possono apparire come maiuscole e minuscole ma l'ordinamento non deve fare distinzione tra maiuscole e minuscole, applica la UPPER()funzione al risultato finale di tutti i segmenti (in modo che debba essere eseguita una sola volta e non per segmento). Ciò consentirà un corretto ordinamento dato il confronto binario della colonna di ordinamento.
  • Creare un AFTER INSERT, UPDATEtrigger sulla tabella che chiama l'UDF per impostare la colonna di ordinamento. Per migliorare le prestazioni, utilizzare la UPDATE()funzione per determinare se questa colonna codice è anche nella SETclausola della UPDATEdichiarazione (semplicemente RETURNse false), e poi unire le INSERTEDe DELETEDpseudotabelle sulla colonna codice solo le righe processo che devono variazioni del valore del codice . Assicurati di specificare COLLATE Latin1_General_100_BIN2su quella condizione JOIN per garantire l'accuratezza nel determinare se c'è una modifica.
  • Crea un indice sulla nuova colonna di ordinamento.

Esempio:

P7B18   -> "P     000007B     000018"
P12B3   -> "P     000012B     000003"
P12B3C8 -> "P     000012B     000003C     000008"

In questo approccio, è possibile ordinare tramite:

ORDER BY tbl.SortColumn

E puoi eseguire il filtraggio dell'intervallo tramite:

WHERE tbl.SortColumn BETWEEN dbo.MyUDF('P7B18') AND dbo.MyUDF('P12B3')

o:

DECLARE @RangeStart VARCHAR(50),
        @RangeEnd VARCHAR(50);
SELECT @RangeStart = dbo.MyUDF('P7B18'),
       @RangeEnd = dbo.MyUDF('P12B3');

WHERE tbl.SortColumn BETWEEN @RangeStart AND @RangeEnd

Sia il filtro che ORDER BYil WHEREfiltro dovrebbero usare le regole di confronto binarie definite a SortColumncausa della precedenza delle regole di confronto.

I confronti di uguaglianza verrebbero comunque effettuati sulla colonna del valore originale.


Altri pensieri:

  • Utilizzare un UDT SQLCLR. Ciò potrebbe funzionare, sebbene non sia chiaro se presenti un guadagno netto rispetto all'approccio sopra descritto.

    Sì, un SQLCLR UDT può far sovrascrivere i suoi operatori di confronto con algoritmi personalizzati. Gestisce situazioni in cui il valore viene confrontato con un altro valore che è già lo stesso tipo personalizzato o che deve essere convertito implicitamente. Questo dovrebbe gestire il filtro intervallo in una WHEREcondizione.

    Per quanto riguarda l'ordinamento dell'UDT come un normale tipo di colonna (non una colonna calcolata), ciò è possibile solo se l'UDT è "byte ordinato". Essere "byte ordinati" significa che la rappresentazione binaria dell'UDT (che può essere definita nell'UDT) ordina naturalmente nell'ordine appropriato. Supponendo che la rappresentazione binaria sia gestita in modo simile all'approccio sopra descritto per la colonna VARCHAR (50) che ha segmenti a lunghezza fissa che sono riempiti, che si qualificherebbe. Oppure, se non fosse facile garantire che la rappresentazione binaria fosse naturalmente ordinata nel modo corretto, è possibile esporre un metodo o una proprietà dell'UDT che genera un valore che sarebbe correttamente ordinato e quindi creare una PERSISTEDcolonna calcolata su tale metodo o proprietà. Il metodo deve essere deterministico e contrassegnato come IsDeterministic = true.

    I vantaggi di questo approccio sono:

    • Non è necessario un campo "valore originale".
    • Non è necessario chiamare un UDF per inserire i dati o confrontare i valori. Supponendo che il Parsemetodo dell'UDT assuma il P7B18valore e lo converta, allora dovresti essere in grado di inserire semplicemente i valori come P7B18. E con il metodo di conversione implicita impostato nell'UDT, la condizione WHERE consentirebbe anche di utilizzare semplicemente P7B18`.

    Le conseguenze di questo approccio sono:

    • La semplice selezione del campo restituirà la rappresentazione binaria, se si utilizza l'UDT ordinato dal byte come tipo di dati della colonna. O se si utilizza una PERSISTEDcolonna calcolata su una proprietà o un metodo dell'UDT, si otterrà la rappresentazione restituita dalla proprietà o dal metodo. Se si desidera il P7B18valore originale , è necessario chiamare un metodo o una proprietà dell'UDT codificata per restituire tale rappresentazione. Dal momento che è necessario ignorare il ToStringmetodo comunque, questo è un buon candidato per fornire questo.
    • Non è chiaro (almeno per me in questo momento poiché non ho testato questa parte) quanto sia facile / difficile apportare modifiche alla rappresentazione binaria. La modifica della rappresentazione memorizzabile e ordinabile potrebbe richiedere l'abbandono e l'aggiunta del campo. Inoltre, la caduta dell'Assemblea contenente l'UDT fallirebbe se utilizzata in entrambi i modi, quindi si dovrebbe assicurarsi che non ci fosse nient'altro nell'Assemblea oltre a questo UDT. Puoi ALTER ASSEMBLYsostituire la definizione, ma ci sono alcune restrizioni a riguardo.

      D'altra parte, il VARCHAR()campo è costituito da dati che sono disconnessi dall'algoritmo, quindi richiederebbe solo l'aggiornamento della colonna. E se ci sono decine di milioni di righe (o più), ciò può essere fatto in un approccio discontinuo.

  • Implementare la libreria ICU che in realtà consente di eseguire questo ordinamento alfanumerico. Sebbene altamente funzionale, la libreria è disponibile solo in due lingue: C / C ++ e Java. Ciò significa che potrebbe essere necessario apportare alcune modifiche per farlo funzionare in Visual C ++ o c'è la possibilità che il codice Java possa essere convertito in MSIL usando IKVM . Esistono uno o due progetti lato .NET collegati su quel sito che forniscono un'interfaccia COM a cui è possibile accedere nel codice gestito, ma credo che non siano stati aggiornati da un po 'e non li ho provati. La scommessa migliore qui sarebbe quella di gestirla nel livello dell'app con l'obiettivo di generare chiavi di ordinamento. Le chiavi di ordinamento verrebbero quindi salvate in una nuova colonna di ordinamento.

    Questo potrebbe non essere l'approccio più pratico. Tuttavia, è ancora molto bello che esista una tale abilità. Ho fornito una guida più dettagliata di un esempio di questo nella seguente risposta:

    Esiste un confronto per ordinare le seguenti stringhe nel seguente ordine 1,2,3,6,10,10A, 10B, 11?

    Ma il modello trattato in quella domanda è un po 'più semplice. Per un esempio che mostra che il tipo di modello trattato in questa domanda funziona anche, vai alla pagina seguente:

    Demo di confronto ICU

    In "Impostazioni", imposta l'opzione "numerica" ​​su "on" e tutti gli altri dovrebbero essere impostati su "default". Successivamente, a destra del pulsante "ordina", deseleziona l'opzione per "punti di forza diff" e controlla l'opzione per "ordina chiavi". Quindi sostituire l'elenco di elementi nell'area di testo "Input" con il seguente elenco:

    P12B22
    P7B18
    P12B3
    as456456hgjg6786867
    P7Bb19
    P7BA19
    P7BB19
    P007B18
    P7Bb20
    P7Bb19z23
    

    Fai clic sul pulsante "ordina". L'area di testo "Output" dovrebbe visualizzare quanto segue:

    as456456hgjg6786867
        29 4D 0F 7A EA C8 37 35 3B 35 0F 84 17 A7 0F 93 90 , 0D , , 0D .
    P7B18
        47 0F 09 2B 0F 14 , 08 , FD F1 , DC C5 DC 05 .
    P007B18
        47 0F 09 2B 0F 14 , 08 , FD F1 , DC C5 DC 05 .
    P7BA19
        47 0F 09 2B 29 0F 15 , 09 , FD FF 10 , DC C5 DC DC 05 .
    P7Bb19
        47 0F 09 2B 2B 0F 15 , 09 , FD F2 , DC C5 DC 06 .
    P7BB19
        47 0F 09 2B 2B 0F 15 , 09 , FD FF 10 , DC C5 DC DC 05 .
    P7Bb19z23
        47 0F 09 2B 2B 0F 15 5B 0F 19 , 0B , FD F4 , DC C5 DC 08 .
    P7Bb20
        47 0F 09 2B 2B 0F 16 , 09 , FD F2 , DC C5 DC 06 .
    P12B3
        47 0F 0E 2B 0F 05 , 08 , FD F1 , DC C5 DC 05 .
    P12B22
        47 0F 0E 2B 0F 18 , 08 , FD F1 , DC C5 DC 05 .
    

    Le chiavi di ordinamento sono strutturate in più campi, separate da virgole. Ogni campo deve essere ordinato in modo indipendente, in modo che presenti un altro piccolo problema da risolvere se è necessario implementarlo in SQL Server.


** In caso di dubbi sulle prestazioni relative all'uso delle funzioni definite dall'utente, si noti che gli approcci proposti ne fanno un uso minimo. In effetti, il motivo principale per l'archiviazione del valore normalizzato era quello di evitare di chiamare un UDF per ogni riga di ogni query. Nell'approccio primario, l'UDF viene utilizzato per impostare il valore di SortColumn, e ciò viene fatto solo su INSERTe UPDATEtramite il trigger. La selezione di valori è molto più comune dell'inserimento e dell'aggiornamento e alcuni valori non vengono mai aggiornati. Per ogni SELECTquery che utilizza il SortColumnfiltro for range nella WHEREclausola, l'UDF è necessario solo una volta per ciascuno dei valori range_start e range_end per ottenere i valori normalizzati; l'UDF non viene chiamato per riga.

Per quanto riguarda l'UDT, l'utilizzo è effettivamente lo stesso dell'UDF scalare. Significato, l'inserimento e l'aggiornamento chiamerebbe il metodo di normalizzazione una volta per ogni riga per impostare il valore. Quindi, il metodo di normalizzazione verrebbe chiamato una volta per query per ogni range_start e range_value in un filtro intervallo, ma non per riga.

Un punto a favore della gestione della normalizzazione interamente in un UDF SQLCLR è che dato che non sta facendo alcun accesso ai dati ed è deterministico, se è contrassegnato come IsDeterministic = true, allora può partecipare a piani paralleli (che potrebbero aiutare le operazioni INSERTe UPDATE) mentre un T-SQL UDF impedirà l'utilizzo di un piano parallelo.

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.