SQL Server w logistyce

Sortowanie liczbowe kodów zapisanych jako tekst

Kolumna nvarchar sortuje kody znak po znaku, więc lista pokazuje 1, 10, 100, 11, 2. Wzorzec z ORDER BY i TRY_CAST ustawia kolejność liczbową bez zmiany typu kolumny.

sql.server.net.pl/sql-server/
Abstrakcyjna panorama wieżowców nocą z unoszącymi się kafelkami danych w odcieniach pomarańczu i błękitu
Abstrakcyjna panorama wieżowców nocą z unoszącymi się kafelkami danych w odcieniach pomarańczu i błękitu
W skrócie
  • Problem: kolumna nvarchar sortuje kody znak po znaku, więc 10 stoi przed 2.
  • Wzorzec: ORDER BY z TRY_CAST do decimal(38,0) układa kody liczbowo, a nieliczbowe trafiają na koniec.
  • DISTINCT: zapytanie z SELECT DISTINCT wymaga tabeli pochodnej, bo wyrażenie z ORDER BY musi być na liście wyników.
  • Test: wynik jest taki sam na poziomach zgodności 130 i 160.

Kolumna nvarchar, w której kody są numerami porządkowymi, sortuje się znak po znaku, więc lista wyboru pokazuje kody w kolejności 1, 10, 100, 101, 11, 2. Programista T-SQL, który widzi taką listę, chce kolejności liczbowej bez zmiany typu kolumny. Pokazujemy wzorzec sortowania liczbowego kodów zapisanych jako tekst: ORDER BY z TRY_CAST do decimal(38,0) ustawia kolejność liczbową i odsuwa kody nieliczbowe na koniec. Nazwy tabel i kolumn w kodzie są przykładowe.

Sortowanie liczbowe kodów zapisanych jako tekst wymaga ORDER BY z TRY_CAST do decimal(38,0). Funkcja zwraca NULL zamiast błędu, flaga w pierwszym członie odsuwa takie kody na koniec, a kod jako tekst rozstrzyga remisy. Gdy zapytanie ma SELECT DISTINCT, sortowanie trzeba przenieść poza tabelę pochodną.

Wzorzec wymaga trzech decyzji, które opisują kolejne sekcje.

  1. Zamiana tekstu na liczbę: TRY_CAST zwraca NULL zamiast błędu.
  2. Kod, którego nie da się zamienić: flaga w pierwszym członie ORDER BY odsuwa go na koniec.
  3. Zapytanie z SELECT DISTINCT: wyrażenie z ORDER BY musi być na liście wyników, więc zapytanie trafia do tabeli pochodnej.

Tabela w kodzie nazywa się artykul_przyklad, a jej kolumna kod ma typ nvarchar(50). Kody w przykładzie to liczby, jeden zapis nieliczbowy (A-5) i jeden zapis ze spacjami po obu stronach. Te kody pokazują obie strony wzorca: sortowanie liczb i obsługę wartości, które liczbami nie są.

Tekstowy ORDER BY układa kody znak po znaku

Pierwsze zapytanie sortuje kolumnę kod bez żadnej konwersji. Kolumna jest tekstowa, więc porównanie idzie znak po znaku i kod 10 stoi przed kodem 2, bo cyfra 1 poprzedza cyfrę 2. Kod ze spacjami po obu stronach trafia na początek, bo spacja sortuje się przed cyframi. Kolejność tekstowa zależy od sortowania bazy, a w teście było to Polish_CI_AS.

Blok poniżej tworzy tabelę z ośmioma kodami i zawiera dwa zapytania: tekstowe i liczbowe. Wszystkie osiem wierszy wstawia jedno polecenie INSERT, a kolumna kod ma typ nvarchar(50), więc każdy kod, także A-5, mieści się w tej samej kolumnie. Zapytanie liczbowe opisujemy w następnej sekcji.

CREATE TABLE dbo.artykul_przyklad (kod nvarchar(50) NOT NULL, nazwa nvarchar(100) NOT NULL);
INSERT INTO dbo.artykul_przyklad (kod, nazwa) VALUES
    (N'1', N'a'), (N'2', N'b'), (N'10', N'c'), (N'100', N'd'),
    (N'101', N'e'), (N'11', N'f'), (N'A-5', N'g'), (N' 3 ', N'h');
SELECT kod FROM dbo.artykul_przyklad ORDER BY kod;
SELECT x.kod
FROM (SELECT DISTINCT kod FROM dbo.artykul_przyklad) AS x
ORDER BY
    CASE WHEN TRY_CAST(NULLIF(LTRIM(RTRIM(x.kod)), N'') AS decimal(38,0)) IS NULL THEN 1 ELSE 0 END,
    TRY_CAST(NULLIF(LTRIM(RTRIM(x.kod)), N'') AS decimal(38,0)),
    x.kod;

Przykład ilustracyjny, nazwy wymyślone. Tabela z ośmioma kodami i dwa zapytania: tekstowe, potem liczbowe.

  • Kody liczbowe: sześć kodów: 1, 2, 10, 11, 100, 101.
  • Kody specjalne: dwa kody: A-5 jako zapis nieliczbowy i „ 3 ” ze spacjami po obu stronach.

ORDER BY z TRY_CAST układa kody liczbowo i odsuwa nieliczbowe na koniec

Drugie zapytanie ma w ORDER BY trzy człony, które działają po kolei. Pierwszy rozdziela liczby i kody nieliczbowe, drugi porządkuje liczby, a trzeci rozstrzyga kody o tej samej wartości.

  1. Flaga nieliczbowych: wyrażenie CASE zwraca 1 dla kodu nieliczbowego i 0 dla liczby, więc kody nieliczbowe lądują na końcu.
  2. Wartość liczbowa: TRY_CAST do decimal(38,0) porządkuje liczby według wartości.
  3. Kod jako tekst: sam kod rozstrzyga remisy.

Funkcje LTRIM i RTRIM usuwają spacje z obu stron, a NULLIF zamienia pusty tekst na NULL. Kod ze spacjami po obu stronach jest więc traktowany jak liczba 3, a pusty tekst nie powoduje błędu konwersji. Wynik w teście to kolejność 1, 2, 3 (ze spacjami), 10, 11, 100, 101, A-5. Tabela zestawia obie kolejności na tych samych ośmiu kodach.

Kolejność ośmiu kodów w tekstowym ORDER BY i we wzorcu z TRY_CAST
PozycjaTekstowy ORDER BYWzorzec z TRY_CAST
1„ 3 ”1
212
310„ 3 ”
410010
510111
611100
72101
8A-5A-5

Kolejność ośmiu kodów w obu zapytaniach.

Kolejność tekstowa i liczbowa tych samych kodówSchemat z dwiema listami po osiem kodów. Lewa lista, tekstowy ORDER BY: kod 3 ze spacjami, 1, 10, 100, 101, 11, 2, A-5. Prawa lista, wzorzec z TRY_CAST do decimal(38,0): 1, 2, kod 3 ze spacjami, 10, 11, 100, 101, A-5. Strzałka łączy te same kody. Kolejność tekstowa i liczbowa tych samych kodów Kolumna nvarchar jest porównywana znak po znaku, więc 10 stoi przed 2. ORDER BY kod 3 (ze spacjami) 1 10 100 101 11 2 A-5 ORDER BY z TRY_CAST do decimal(38,0) 1 2 3 (ze spacjami) 10 11 100 101 A-5 te same kody Dane przykładowe. Wynik testu na poziomach zgodności 130 i 160 był identyczny.
Dwie listy kodów: tekstowy ORDER BY daje 1, 10, 100, 11, a wzorzec z TRY_CAST daje 1, 2, 3, 10, 11, 100, 101.

Schemat pokazuje tę samą zależność w układzie dwóch kolumn. Po lewej stoi wynik tekstowy, po prawej liczbowy, a strzałka łączy te same kody. Kod A-5 stoi w obu kolejnościach na końcu, ale z innego powodu: w kolejności tekstowej wypada ostatni, a we wzorcu odsuwa go flaga.

SELECT DISTINCT wymaga tabeli pochodnej, gdy sortuje po wyrażeniu

Pierwsza wersja zapytania liczbowego, która łączy SELECT DISTINCT z ORDER BY po wyrażeniu, kończy się błędem 145. Silnik wymaga, żeby każde wyrażenie z ORDER BY występowało na liście wyników, gdy zapytanie ma DISTINCT. Wyrażenie z TRY_CAST nie jest kolumną z listy wyników, więc zapytanie nie przechodzi. Na obu poziomach zgodności komunikat był taki sam.

ORDER BY items must appear in the select list if SELECT DISTINCT is specified.

Rozwiązaniem jest tabela pochodna: zapytanie z DISTINCT stoi w nawiasie w klauzuli FROM, a ORDER BY stoi na zewnątrz i sortuje już kolumnę kod z listy wyników tabeli pochodnej. Tak zbudowane jest zapytanie 2 z pierwszego bloku. Drugi blok pokazuje wersję, która kończy się błędem.

SELECT DISTINCT kod
FROM dbo.artykul_przyklad
ORDER BY TRY_CAST(NULLIF(LTRIM(RTRIM(kod)), N'') AS decimal(38,0));

Przykład ilustracyjny. To zapytanie kończy się błędem 145.

  • Wersja z błędem: SELECT DISTINCT i ORDER BY po wyrażeniu w jednym zapytaniu.
  • Wersja poprawna: SELECT DISTINCT w tabeli pochodnej, a ORDER BY na zewnątrz.

TRY_CAST zamienia część zapisów na NULL, a inne zaokrągla

TRY_CAST do decimal(38,0) nie zgłasza błędu dla zapisów, których nie rozumie. Zwraca NULL, więc takie kody trafiają na koniec listy. Liczby z ułamkiem zaokrągla do całości, a nie odrzuca. Zapis 1.5 daje 2, a trzy rodzaje zapisów dają NULL: z przecinkiem, z notacją wykładniczą i ze spacją w środku.

Trzeci blok sprawdza osiem zapisów jednym zapytaniem. Tabela po bloku zawiera wynik. Zapisy 007 i 7 dają tę samą wartość 7, a kolejność między nimi rozstrzyga trzeci człon ORDER BY, czyli kod jako tekst. Liczba z 39 cyfr przekracza zakres decimal(38,0) i także daje NULL.

SELECT v.kod,
       TRY_CAST(NULLIF(LTRIM(RTRIM(v.kod)), N'') AS decimal(38,0)) AS liczba
FROM (VALUES (N'007'), (N'7'), (N'1.5'), (N'2,5'), (N'1e3'), (N'1 000'), (N''),
             (REPLICATE(N'9', 39))) AS v(kod);

Przykład ilustracyjny. Osiem zapisów w jednym zapytaniu.

Wynik TRY_CAST do decimal(38,0) dla ośmiu zapisów
ZapisWynik TRY_CAST
0077
77
1.52
2,5NULL
1e3NULL
1 000NULL
tekst pustyNULL
39 cyfrNULL

Wynik konwersji dla ośmiu zapisów.

Kody o tej samej wartości rozstrzyga człon tekstowy

Zapisy 007 i 7 mają tę samą wartość liczbową, więc dwa pierwsze człony ORDER BY nie odróżniają ich od siebie. Rozstrzyga trzeci człon, czyli kod jako tekst. Bez niego kolejność dwóch takich kodów nie byłaby określona i mogłaby się zmieniać między wykonaniami zapytania.

  • Remis wartości: zapisy 007 i 7 mają wartość 7.
  • Rozstrzygnięcie: trzeci człon ORDER BY, czyli kod jako tekst.

Człon tekstowy jest więc tanim zabezpieczeniem przed niestabilną kolejnością. Gdy kody o tej samej wartości liczbowej są wykluczone, człon można pominąć. Gdy są możliwe, zostaje potrzebny. Pułapką jest też założenie, że decimal(38,0) obsługuje ułamki: konwersja zaokrągla je do całości, więc dwa różne zapisy mogą trafić do tej samej wartości.

Wynik jest identyczny na poziomach zgodności 130 i 160

Test wykonano w jednorazowej bazie na dwóch poziomach zgodności. Kolejność tekstowa i kolejność liczbowa wyszły identyczne, podobnie jak granice TRY_CAST, a błąd 145 pojawił się na obu poziomach z tym samym komunikatem. Różnić może się tylko kolejność tekstowa, bo zależy od sortowania bazy, a nie od poziomu zgodności.

Wynik testu na poziomach zgodności 130 i 160
SprawdzeniePoziom 130Poziom 160
Kolejność tekstowa„ 3 ”, 1, 10, 100, 101, 11, 2, A-5„ 3 ”, 1, 10, 100, 101, 11, 2, A-5
Kolejność liczbowa1, 2, „ 3 ”, 10, 11, 100, 101, A-51, 2, „ 3 ”, 10, 11, 100, 101, A-5
SELECT DISTINCT z ORDER BY po wyrażeniubłąd 145błąd 145
Zapis 1.5 w TRY_CAST22

Wyniki są takie same na obu poziomach.

Sposób sprawdzenia jest prosty. Trzy bloki uruchamia się w tej kolejności w jednorazowej bazie, porównuje wyniki z tabelami i usuwa bazę po teście. Zapytanie z błędem 145 kończy się komunikatem, a pozostałe bloki dają tabele z wynikami. Pokrewne wzorce, które także dotyczą ustawień i zapisu w T-SQL, opisujemy w artykułach o QUOTED_IDENTIFIER w procedurach i o aktualizacji nagłówka bez zerowania pozycji.

Przykład (dane wymyślone): lista wyboru artykułów pokazuje kody w kolejności 1, 10, 100, 101, 11, 2, więc osoba szukająca kodu 2 przewija listę do końca. Programista zmienia ORDER BY na wzorzec z TRY_CAST, tabelę z DISTINCT zamyka w tabeli pochodnej i dostaje kolejność 1, 2, 10, 11, 100, 101. Kod A-5 trafia na koniec, bo nie jest liczbą. Kod ze spacjami po obu stronach sortuje się jak liczba, bo funkcje LTRIM i RTRIM go oczyszczają.

Pytania do analizy

Przed wdrożeniem takiego sortowania odpowiedz na cztery pytania.

  1. Kiedy wartość 1.5 zapisana w kolumnie kodu zmieni miejsce na liście przez zaokrąglenie do decimal(38,0)?
  2. Dlaczego SELECT DISTINCT zgłasza błąd 145 i czy tabela pochodna jest jedynym wyjściem?
  3. Jakie kody trafią na koniec listy i czy to jest zachowanie, którego oczekuje użytkownik?
  4. Czy człon tekstowy na końcu ORDER BY jest potrzebny, gdy kody o tej samej wartości liczbowej są niemożliwe?
FAQ

Najczęstsze pytania o sortowanie liczbowe kodów

01

Dlaczego kody sortują się jako 1, 10, 100?

Kolumna nvarchar jest porównywana znak po znaku, więc 10 stoi przed 2.

02

Czy TRY_CAST zgłosi błąd dla kodu A-5?

Nie. Zwraca NULL, a flaga w pierwszym członie ORDER BY odsuwa taki kod na koniec.

03

Czy wynik zależy od poziomu zgodności?

W teście wynik był identyczny na poziomach 130 i 160. Kolejność tekstowa zależy natomiast od sortowania bazy, w teście było to Polish_CI_AS.

04

Dlaczego potrzebna jest tabela pochodna?

SELECT DISTINCT wymaga, żeby wyrażenie z ORDER BY było na liście wyników. W tabeli pochodnej kolumna jest na liście, a sortowanie stoi na zewnątrz.

05

Czy ORDER BY po wyrażeniu użyje indeksu?

Ten test tego nie sprawdzał, bo nie mierzył planu wykonania. Wyrażenie z TRY_CAST nie jest kolumną, więc odpowiedź wymaga osobnego pomiaru.

Słownik pojęć

Słownik pojęć

Pojęcia z zapytań i przykładu opisanych w artykule.

TTRY_CAST
Konwersja typu, która zamiast błędu zwraca NULL.
Ddecimal(38,0)
Liczba całkowita do 38 cyfr. Ułamki są zaokrąglane przy konwersji z tekstu.
TTabela pochodna
Podzapytanie w klauzuli FROM, którego kolumn może użyć zewnętrzny ORDER BY.
NNULLIF
Zwraca NULL, gdy wartość jest równa drugiemu argumentowi, tu pustemu tekstowi.
FFlaga nieliczbowych
Wyrażenie CASE zwracające 0 dla liczb i 1 dla pozostałych kodów. Jest pierwszym członem ORDER BY.

Zobacz to w praktyce

Sprawdź, jak systemy SoftwareStudio oparte na MS SQL Server wspierają ten obszar Twojej firmy.