SQL Server w logistyce

STRING_AGG w raporcie: wiele numerów w jednej kolumnie, drukowanych raz na pozycję

STRING_AGG skleja numery seryjne i partie w jedną kolumnę, a ROW_NUMBER wskazuje jedyny wiersz pozycji, w którym ta kolumna się pojawia. Przykład sprawdzony na SQL Server 2022.

sql.server.net.pl/sql-server/
Zbliżenie tabeli z rozżarzonymi pomarańczowymi komórkami
Zbliżenie tabeli z rozżarzonymi pomarańczowymi komórkami
W skrócie
  • Funkcja: STRING_AGG ... WITHIN GROUP skleja aktywne numery pozycji w jedną kolumnę, od poziomu zgodności 110.
  • Wiersz: ROW_NUMBER wskazuje pierwszy wiersz pozycji, w pozostałych wierszach stoi NULL.
  • Limit: wynik ma 8000 bajtów, dopóki składnik nie zostanie rzutowany na typ max.
  • Kontrola: dwa zapytania sprawdzają, że numery stoją w jednym wierszu i że ich liczba zgadza się z tabelą.

STRING_AGG w zapytaniu raportu skleja numery seryjne i partie w jedną kolumnę, gdy raport drukuje wiersz na paletę, a numery należą do pozycji. ROW_NUMBER wybiera wtedy jedyny wiersz pozycji, w którym ta kolumna ma stanąć, żeby numery nie powtarzały się przy każdej palecie.

Funkcja STRING_AGG z klauzulą WITHIN GROUP skleja aktywne numery pozycji w jedną kolumnę w ustalonej kolejności. Funkcja okna ROW_NUMBER z podziałem na pozycje wskazuje pierwszy wiersz palety i tylko tam wstawia sklejony tekst. Pozostałe wiersze pozycji dostają NULL, więc numery drukują się dokładnie raz.

To wzorzec z nazwami przykładowymi, bez związku z konkretnym wdrożeniem. Napisaliśmy go od nowa i uruchomiliśmy na SQL Server 2022 Express w jednorazowej bazie, a wyniki w artykule są wynikami tego uruchomienia. Tabela numerów ma tu inną postać niż w artykule o indeksie filtrowanym, bo to osobne przykłady. Szersze tło o budowie zapytań dla wydruków znajdziesz w opisie raportowania w SQL, a tutaj zajmujemy się jednym wzorcem.

Raport z wierszem na paletę i numerami należącymi do pozycji

Przykład ma trzy tabele. Tabela pozycja_dokumentu opisuje pozycję dokumentu: numer dokumentu i artykuł. Tabela pozycja_paleta ma wiele wierszy na pozycję, bo ten sam artykuł może leżeć na kilku paletach, a dwa wiersze mogą nawet wskazywać tę samą paletę. Tabela numery_pozycji trzyma numery powiązane z pozycją, nie z paletą, i właśnie ta różnica rodzi cały problem: złączenie pozycji z paletami powiela numery tyle razy, ile jest wierszy palet.

Kolumna typ_numeru w tabeli numery_pozycji przyjmuje trzy wartości, opisane w tabeli poniżej. Wartość 0 w kolumnie czy_aktywny oznacza numer usunięty miękko: wiersz zostaje w tabeli, a raport go pomija.

Typy wierszy w tabeli numery_pozycji
Typ numeruZnaczenieCo robi w raporcie
SERnumer seryjnydrukuje się jako sam numer
PARpartiadrukuje się jako numer z ilością w nawiasie
BRAKznacznik braku numerównie trafia do kolumny, służy tylko do adnotacji

Przykład. Znacznik BRAK jest elementem przykładu i oznacza pozycję, dla której numerów nie przewidziano.

CREATE TABLE dbo.pozycja_dokumentu (
    id_pozycji   int          PRIMARY KEY,
    nr_dokumentu varchar(20)  NOT NULL,
    artykul      nvarchar(40) NOT NULL);
CREATE TABLE dbo.pozycja_paleta (
    id_wiersza int           PRIMARY KEY,
    id_pozycji int           NOT NULL REFERENCES dbo.pozycja_dokumentu,
    nr_palety  varchar(20)   NOT NULL,
    ilosc      decimal(15,3) NOT NULL);
CREATE TABLE dbo.numery_pozycji (
    id             int IDENTITY  PRIMARY KEY,
    id_pozycji     int           NOT NULL REFERENCES dbo.pozycja_dokumentu,
    typ_numeru     varchar(4)    NOT NULL,                 -- SER numer seryjny, PAR partia, BRAK znacznik braku numerów
    wartosc_numeru nvarchar(100) NOT NULL DEFAULT N'',
    ilosc          decimal(15,3) NULL,
    czy_aktywny    bit           NOT NULL DEFAULT 1);       -- 0 = usunięty miękko

Przykład ilustracyjny. Nazwy tabel i kolumn są wymyślone.

Dane testowe opisują jeden dokument, DOK 26-1001, z czterema pozycjami. Artykuł A ma trzy wiersze palet, w tym dwa na PAL-0001. Ma też cztery aktywne numery seryjne i jeden usunięty. Artykuł B ma dwa wiersze palet i dwie partie. Artykuł C ma wyłącznie wiersz typu BRAK, a Artykuł D nie ma żadnego wiersza w tabeli numerów.

INSERT dbo.pozycja_dokumentu VALUES
 (1,'DOK 26-1001',N'Artykuł A'), (2,'DOK 26-1001',N'Artykuł B'),
 (3,'DOK 26-1001',N'Artykuł C'), (4,'DOK 26-1001',N'Artykuł D');
INSERT dbo.pozycja_paleta VALUES
 (1,1,'PAL-0001',2), (2,1,'PAL-0001',1), (3,1,'PAL-0002',1),
 (4,2,'PAL-0003',30), (5,2,'PAL-0004',22.5), (6,3,'PAL-0005',5), (7,4,'PAL-0006',8);
INSERT dbo.numery_pozycji (id_pozycji, typ_numeru, wartosc_numeru, ilosc, czy_aktywny) VALUES
 (1,'SER',N'SN-000125',NULL,1), (1,'SER',N'SN-000123',NULL,1),
 (1,'SER',N'SN-000126',NULL,1), (1,'SER',N'SN-000124',NULL,1),
 (1,'SER',N'SN-000999',NULL,0), (2,'PAR',N'LOT-0102',30,1),
 (2,'PAR',N'LOT-0101',22.5,1),  (3,'BRAK',N'',NULL,1);

Przykład ilustracyjny. Wartości są wymyślone.

Numery w jednej kolumnie, raz na pozycjęSchemat: tabela numerów jest sklejana funkcją STRING_AGG w jedną kolumnę na pozycję, a ROW_NUMBER wskazuje pierwszy wiersz pozycji w tabeli palet. W wyniku raportu kolumna numery jest wypełniona tylko w pierwszym z trzech wierszy pozycji, w pozostałych ma NULL.STRING_AGG skleja numery, a ROW_NUMBER wskazuje wiersz, w którym mają stanąćRaport drukuje wiersz na paletę, a numery seryjne i partie należą do pozycji.numery_pozycji (SER, PAR, BRAK)numery należą do pozycji, nie do paletyusunięty numer ma czy_aktywny = 0pozycja_paleta (wiele wierszy na pozycję)dwa wiersze mogą leżeć na tej samejpalecieSTRING_AGG ... WITHIN GROUP(ORDER BY typ_numeru, wartosc_numeru)jedna kolumna na pozycjętylko aktywne numery typu SER i PARROW_NUMBER() ... = 1wskazuje pierwszy wiersz pozycjipozostałe wiersze pozycji: NULLnr paletyilośćnumeryPAL-00012SN-000123, SN-000124,SN-000125, SN-000126PAL-00011NULLPAL-00021NULLWynik raportu: jedna pozycja, trzy wierszesklejone numerytylko wiersz nr 1Raport drukuje jeden wiersz na wiersz palety, a numery należą do pozycji.NULL w drugim i trzecim wierszu oznacza, że numery stoją tylko w pierwszym wierszu pozycji.Drugi wiersz na tej samej palecie nie dubluje numerów, bo wiersz wybiera ROW_NUMBER, a nie MIN.
Schemat: numery z tabeli skleja STRING_AGG w jedną kolumnę, a ROW_NUMBER wybiera pierwszy wiersz pozycji.

Jedna kolumna z numerami przez STRING_AGG i WITHIN GROUP

Numery skleja wyrażenie tabelowe n: jedno zapytanie grupujące po pozycji. STRING_AGG przyjmuje wyrażenie do sklejenia i separator, tutaj przecinek ze spacją. Klauzula WITHIN GROUP (ORDER BY typ_numeru, wartosc_numeru) ustala kolejność elementów wewnątrz sklejonego tekstu, bo sama funkcja jej nie gwarantuje.

Warunek WHERE przepuszcza wyłącznie aktywne numery typów SER i PAR. Numer usunięty miękko oraz wiersz typu BRAK nie wchodzą do kolumny, co potwierdził test: usunięty SN-000999 nie pojawił się w wyniku.

Seria drukuje się jako sam numer, a partia jako numer z ilością. Wyrażenie CASE wybiera format, a FORMAT(ilosc, '0.###', 'pl-PL') zamienia 22,5 na tekst 22,5, a 30 na tekst 30, bez zbędnych zer. Trzeci argument z kulturą ma znaczenie, o czym piszemy w sekcji o pułapkach.

Cały składnik jest owinięty w CONVERT(nvarchar(max), ...). To świadoma decyzja: bez rzutowania na typ max wynik funkcji ma limit 8000 bajtów, a jego przekroczenie przerywa zapytanie błędem.

CREATE OR ALTER VIEW dbo.raport_numerow AS
WITH n AS (
    SELECT id_pozycji,
           STRING_AGG(CONVERT(nvarchar(max),
                      CASE WHEN typ_numeru = 'PAR'
                           THEN wartosc_numeru + N' (' + FORMAT(ilosc, '0.###', 'pl-PL') + N')'
                           ELSE wartosc_numeru END), N', ')
                      WITHIN GROUP (ORDER BY typ_numeru, wartosc_numeru) AS numery
    FROM dbo.numery_pozycji
    WHERE czy_aktywny = 1 AND typ_numeru IN ('SER', 'PAR')
    GROUP BY id_pozycji
)
SELECT p.id_pozycji, p.nr_dokumentu, p.artykul, w.id_wiersza, w.nr_palety, w.ilosc,
       CASE WHEN ROW_NUMBER() OVER (PARTITION BY w.id_pozycji
                                    ORDER BY w.nr_palety, w.id_wiersza) = 1
            THEN COALESCE(n.numery,
                          CASE WHEN EXISTS (SELECT 1 FROM dbo.numery_pozycji b
                                            WHERE b.id_pozycji = p.id_pozycji
                                              AND b.czy_aktywny = 1 AND b.typ_numeru = 'BRAK')
                               THEN N'numery nie dotyczą' END)
       END AS numery
FROM dbo.pozycja_dokumentu p
JOIN dbo.pozycja_paleta w ON w.id_pozycji = p.id_pozycji
LEFT JOIN n ON n.id_pozycji = p.id_pozycji;

Przykład ilustracyjny. Widok raportu: wyrażenie n skleja numery, a główne zapytanie wybiera wiersz pozycji.

Kolejność wynika z sortowania tekstu po typie numeru i jego wartości. Dla pozycji z samymi seriami oznacza to porządek alfabetyczny, więc SN-000123 stoi przed SN-000124 niezależnie od tego, w jakiej kolejności numery wpisano. Jeśli raport ma zachować kolejność wpisu, potrzebna jest kolumna z numerem kolejnym i sortowanie po niej.

Numery tylko w jednym wierszu pozycji

Główne zapytanie łączy pozycję z wierszami palet i dokłada kolumnę numery. ROW_NUMBER() OVER (PARTITION BY w.id_pozycji ORDER BY w.nr_palety, w.id_wiersza) numeruje wiersze w obrębie pozycji. Wiersz z numerem 1 dostaje sklejony tekst, a pozostałe NULL, bo wyrażenie CASE nie ma gałęzi ELSE.

Drugi składnik sortowania, id_wiersza, nie jest ozdobą. Artykuł A ma dwa wiersze na tej samej palecie PAL-0001. Samo ORDER BY nr_palety nie rozstrzygnęłoby, który z nich jest pierwszy, a dodatkowa kolumna daje jednoznaczny wybór. Podobny problem, czyli wybór jednego wiersza bez pełnego sortowania, opisujemy w artykule o TOP 1 bez ORDER BY.

Kuszącą alternatywą jest porównanie nr_palety z MIN(nr_palety) w obrębie pozycji. W teście takie porównanie postawiło numery w obu wierszach na PAL-0001, czyli w dwóch wierszach pozycji zamiast w jednym.

SELECT p.artykul, w.nr_palety,
       CASE WHEN w.nr_palety = MIN(w.nr_palety) OVER (PARTITION BY w.id_pozycji)
            THEN n.numery END AS numery
FROM dbo.pozycja_dokumentu p
JOIN dbo.pozycja_paleta w ON w.id_pozycji = p.id_pozycji
JOIN (SELECT id_pozycji, STRING_AGG(wartosc_numeru, N', ') AS numery
      FROM dbo.numery_pozycji
      WHERE czy_aktywny = 1 AND typ_numeru = 'SER'
      GROUP BY id_pozycji) n ON n.id_pozycji = p.id_pozycji
ORDER BY p.artykul, w.nr_palety, w.id_wiersza;

Przykład ilustracyjny. Wariant z pułapką: dwa wiersze Artykułu A mają numery, bo oba leżą na PAL-0001.

Poprawne zapytanie raportu zostaje proste: widok czytamy zwykłym SELECT z sortowaniem, w którym id_wiersza również rozstrzyga remis. Poniższa tabela pokazuje wynik uruchomienia.

SELECT nr_dokumentu, artykul, nr_palety, ilosc, numery
FROM dbo.raport_numerow
ORDER BY nr_dokumentu, artykul, nr_palety, id_wiersza;

Przykład ilustracyjny. Zapytanie raportu na widoku.

Wynik zapytania raportu, siedem wierszy
nr_dokumentuartykulnr_paletyiloscnumery
DOK 26-1001Artykuł APAL-00012.000SN-000123, SN-000124, SN-000125, SN-000126
DOK 26-1001Artykuł APAL-00011.000NULL
DOK 26-1001Artykuł APAL-00021.000NULL
DOK 26-1001Artykuł BPAL-000330.000LOT-0101 (22,5), LOT-0102 (30)
DOK 26-1001Artykuł BPAL-000422.500NULL
DOK 26-1001Artykuł CPAL-00055.000numery nie dotyczą
DOK 26-1001Artykuł DPAL-00068.000NULL

Przykład. Wynik uruchomienia na SQL Server 2022 Express.

Wynik raportu w siatceMakieta poglądowa, dane przykładowe. Siatka wyników z kolumnami nr_dokumentu, artykul, nr_palety, ilosc i numery oraz siedmioma wierszami; numery stoją tylko w pierwszym wierszu każdej pozycji, w pozostałych wierszach jest NULL; pod siatką komunikat o siedmiu wierszach.Wyniki zapytanianr_dokumentuartykulnr_paletyiloscnumeryDOK 26-1001Artykuł APAL-00012.000SN-000123, SN-000124, SN-000125, SN-000126DOK 26-1001Artykuł APAL-00011.000NULLDOK 26-1001Artykuł APAL-00021.000NULLDOK 26-1001Artykuł BPAL-000330.000LOT-0101 (22,5), LOT-0102 (30)DOK 26-1001Artykuł BPAL-000422.500NULLDOK 26-1001Artykuł CPAL-00055.000numery nie dotycząDOK 26-1001Artykuł DPAL-00068.000NULL(7 rows affected)pierwszy wiersz pozycjiMakieta poglądowa, dane przykładowe
Makieta poglądowa, dane przykładowe: numery stoją tylko w pierwszym wierszu każdej pozycji, pozostałe wiersze mają NULL.

Pozycja bez numerów i znacznik braku numerów

Pozycja bez numerów ma w wyniku dwie możliwości. Gdy w tabeli numery_pozycji istnieje aktywny wiersz typu BRAK, kolumna numery pokazuje adnotację „numery nie dotyczą”. Gdy nie ma ani numerów, ani znacznika, kolumna zostaje pusta, czyli ma NULL. W wyniku widać więc różnicę między pozycją ze znacznikiem a pozycją bez niego.

Wybór realizuje COALESCE(n.numery, CASE WHEN EXISTS (...) THEN ... END). COALESCE bierze najpierw sklejone numery, a dopiero gdy ich brak, sprawdza wiersz typu BRAK. W przykładzie Artykuł C ma wiersz BRAK i dostaje adnotację, a Artykuł D nie ma nic i zostaje z NULL. Tekst adnotacji jest elementem przykładu: można go zmienić, bo o jego pojawieniu się decyduje wyłącznie warunek EXISTS.

Pułapki STRING_AGG i FORMAT w wyniku raportu

Pierwsza pułapka dotyczy wersji. Według dokumentacji STRING_AGG pojawiła się w SQL Server 2017, więc starsze silniki jej nie mają. Funkcja jest dostępna na każdym poziomie zgodności, ale klauzula WITHIN GROUP wymaga poziomu 110 lub wyższego. Na SQL Server 2022 poziom 100 kończy się przy tworzeniu widoku błędem 102 „Incorrect syntax near '('”, a poziomy od 110 do 160 działają. Samo STRING_AGG bez WITHIN GROUP działa także na poziomie 100, a zależności od poziomu 140 nie ma. Dla SQL Server 2017 nie wykonaliśmy testu.

Druga pułapka to limit wyniku. W teście 500 numerów po 9 znaków w typie varchar dało tekst o długości 5498 znaków. Ten sam zbiór w nvarchar zakończył się błędem 9829, bo limit 8000 bajtów oznacza tam 4000 znaków, a tysiąc numerów w varchar kończy się tym samym błędem. Z rzutowaniem na varchar(max) wynik ma 10998 znaków.

Wynik STRING_AGG ma limit 8000 bajtów, dopóki jeden ze składników nie zostanie rzutowany na typ max.

WITH x AS (
    SELECT TOP (1000) CONCAT('SN-', RIGHT('000000' + CONVERT(varchar(6),
           ROW_NUMBER() OVER (ORDER BY (SELECT NULL))), 6)) AS wartosc_numeru
    FROM sys.all_columns)
SELECT LEN(STRING_AGG(CONVERT(varchar(max), wartosc_numeru), ', ')) AS dlugosc FROM x;

Przykład ilustracyjny. Tysiąc numerów po rzutowaniu na typ max daje tekst o długości 10998 znaków.

Kolejne pułapki zebraliśmy w tabeli. Wszystkie objawy zmierzyliśmy w jednorazowej bazie, a trzy z nich pokazują poniższe zapytania.

Pułapki funkcji STRING_AGG i FORMAT
PułapkaObjaw w teścieRozwiązanie
Limit wynikubłąd 9829 przy 500 numerach w nvarchar i przy 1000 w varcharrzutowanie składnika na typ max
NULL i pusty tekstNULL pominięty, pusty tekst jako element („, SN-1, SN-2”)wykluczyć pusty tekst warunkiem
Kolejność tekstowa„SN-10, SN-100, SN-9”kolumna z numerem kolejnym w ORDER BY albo numery z zerami wiodącymi
Dwie kolejności w jednym zakresiebłąd 8711; stała tekstowa w ORDER BY daje błąd 5309, numer kolumny błąd 5308, podzapytanie błąd 130osobne podzapytania; w ORDER BY kolumna, nie stała
NULL w konkatenacji plusemelement znika po cichuCONCAT albo ISNULL
Język sesji w FORMAT„22.5” zamiast „22,5”trzeci argument z kulturą
MIN zamiast ROW_NUMBERnumery w dwóch wierszach pozycjiROW_NUMBER z kolumną rozstrzygającą remis

Przykład. Siedem pułapek z jednorazowej bazy testowej.

NULL jest w STRING_AGG pomijany, ale pusty tekst liczy się jako element i sortuje na początku, więc daje wynik zaczynający się od przecinka. Same wartości NULL dają NULL. Kolejność tekstowa ustawia SN-10 przed SN-9, bo porównuje znaki, a nie liczby.

-- pusty tekst jest elementem, NULL jest pomijany
SELECT STRING_AGG(wartosc_numeru, N', ') WITHIN GROUP (ORDER BY wartosc_numeru)
FROM (VALUES (N'SN-1'), (N''), (CAST(NULL AS nvarchar(20))), (N'SN-2')) v(wartosc_numeru);

-- kolejność tekstowa
SELECT STRING_AGG(wartosc_numeru, N', ') WITHIN GROUP (ORDER BY wartosc_numeru)
FROM (VALUES (N'SN-9'), (N'SN-10'), (N'SN-100')) v(wartosc_numeru);

Przykład ilustracyjny. Wyniki: „, SN-1, SN-2” oraz „SN-10, SN-100, SN-9”.

Kolejność wpisu da się odtworzyć tylko wtedy, gdy tabela ma kolumnę z numerem kolejnym. Wtedy ORDER BY id w klauzuli WITHIN GROUP daje wynik „SN-9, SN-10, SN-100”.

SELECT STRING_AGG(wartosc_numeru, N', ') WITHIN GROUP (ORDER BY id) AS numery
FROM (VALUES (1, N'SN-9'), (2, N'SN-10'), (3, N'SN-100')) v(id, wartosc_numeru);

Przykład ilustracyjny. Kolejność wpisu zamiast kolejności tekstowej.

Konkatenacja znakiem plus ma własną pułapkę. Gdy ilość partii ma NULL, cały element „numer (ilość)” staje się NULL i znika z wyniku bez żadnego komunikatu. W teście partia z pustą ilością nie trafiła do wyniku, a CONCAT zachował ją jako „LOT-2 ()”. Osobna pułapka dotyczy języka sesji: FORMAT bez trzeciego argumentu daje 22.5 przy angielskim i 22,5 przy polskim, a z kulturą pl-PL zawsze 22,5.

SELECT STRING_AGG(wartosc_numeru + N' (' + FORMAT(ilosc, '0.###', 'pl-PL') + N')', N', ') AS plus,
       STRING_AGG(CONCAT(wartosc_numeru, N' (', FORMAT(ilosc, '0.###', 'pl-PL'), N')'), N', ') AS concat_
FROM (VALUES (N'LOT-1', CAST(5 AS decimal(15,3))),
             (N'LOT-2', CAST(NULL AS decimal(15,3)))) v(wartosc_numeru, ilosc);

Przykład ilustracyjny. Plus gubi element z NULL, CONCAT go zachowuje.

Zapytania kontrolne na wyniku raportu

Po każdej zmianie widoku uruchamiamy dwa zapytania. Pierwsze szuka pozycji, które mają numery w więcej niż jednym wierszu: poprawny widok zwraca 0 wierszy. Drugie porównuje liczbę numerów w kolumnie z liczbą aktywnych numerów typów SER i PAR w tabeli. W teście wyszły pary 4 i 4 dla Artykułu A oraz 2 i 2 dla Artykułu B.

-- kontrola 1: pozycje z numerami w więcej niż jednym wierszu (oczekiwane 0 wierszy)
SELECT id_pozycji, artykul, COUNT(numery) AS wierszy_z_numerami
FROM dbo.raport_numerow
GROUP BY id_pozycji, artykul
HAVING COUNT(numery) > 1;

-- kontrola 2: liczba numerów w kolumnie a liczba aktywnych numerów w tabeli
SELECT r.artykul,
       (LEN(r.numery) - LEN(REPLACE(r.numery, N', ', N''))) / 2 + 1 AS w_kolumnie,
       t.ile AS w_tabeli
FROM dbo.raport_numerow r
JOIN (SELECT id_pozycji, COUNT(*) AS ile
      FROM dbo.numery_pozycji
      WHERE czy_aktywny = 1 AND typ_numeru IN ('SER', 'PAR')
      GROUP BY id_pozycji) t ON t.id_pozycji = r.id_pozycji
WHERE r.numery IS NOT NULL;

Przykład ilustracyjny. Drugie zapytanie zakłada, że sam numer nie zawiera ciągu „, ”.

Przykład: raport dokumentu wydania towarów z numerami

Raport dokumentu DOK 26-1001 zwraca wiersz na paletę. Artykuł A ma cztery numery seryjne od SN-000123 do SN-000126 i trzy wiersze palet, w tym dwa na PAL-0001. Widok z STRING_AGG i ROW_NUMBER pokazuje numery wyłącznie w pierwszym wierszu Artykułu A. Artykuł B ma dwie partie w jednym wierszu. Artykuł C ma wiersz typu BRAK, więc kolumna numery zawiera adnotację „numery nie dotyczą”, a Artykuł D nie ma żadnego wiersza w tabeli numerów i zostaje z NULL. Dwa zapytania kontrolne potwierdzają, że liczba numerów w kolumnie zgadza się z tabelą.

Ten sam wzorzec zadziała z innymi tabelami, o ile pozycja ma stały klucz, a numery znają ten klucz. Dwa kolejne wzorce z tej serii dotyczą tych samych numerów od innej strony: jak policzyć saldo lokalizacji w T-SQL oraz jak pilnować numerów w dokumencie przez indeks filtrowany. Wybór ceny z zakresów cennika jest tematem osobnego artykułu. Przegląd wszystkich artykułów sekcji jest w spisie artykułów o SQL Server, a ogólniejsze podejście do wydruków w opisie raportowania bazy danych.

Pytania przed budową własnego raportu z numerami

Zanim przeniesiesz wzorzec do własnego raportu, odpowiedz sobie na cztery pytania.

  • Wiersz: czy w waszym raporcie jeden wiersz to paleta czy pozycja i w którym wierszu mają stać numery.
  • Rozmiar: ile numerów może mieć największa pozycja i czy wynik zmieści się w polu wydruku.
  • Kolejność: czy numery mają być ułożone alfabetycznie, czy zgodnie z kolejnością wpisu.
  • Usunięte: czy usunięte numery mają zostać w tabeli ze znacznikiem, żeby raport mógł je pominąć.
FAQ

Najczęstsze pytania o STRING_AGG w raporcie

01

Od której wersji SQL Server działa STRING_AGG i od jakiego poziomu zgodności działa klauzula WITHIN GROUP?

Według dokumentacji STRING_AGG pojawiła się w SQL Server 2017. Funkcja działa na każdym poziomie zgodności, ale klauzula WITHIN GROUP wymaga poziomu 110 lub wyższego: na SQL Server 2022 poziom 100 kończy się błędem 102, a poziomy od 110 do 160 działają. Zależności od poziomu 140 nie ma.

02

Dlaczego STRING_AGG kończy się błędem 9829 i jak go uniknąć?

Wynik STRING_AGG ma limit 8000 bajtów, jeśli żaden składnik nie jest typu max. W teście 500 numerów w nvarchar i 1000 numerów w varchar zakończyło się błędem 9829. Rzutowanie składnika na typ max, na przykład CONVERT(varchar(max), numer), usuwa limit: wynik miał wtedy 10998 znaków.

03

Jak ustalić kolejność numerów w sklejonej kolumnie?

Kolejność ustala klauzula WITHIN GROUP (ORDER BY ...). Sortowanie po tekście numeru daje porządek znakowy, czyli „SN-10, SN-100, SN-9”. Kolejność wpisu wymaga kolumny z numerem kolejnym i ORDER BY id, co daje „SN-9, SN-10, SN-100”. Dwa STRING_AGG z różnymi porządkami w tym samym zakresie kończą się błędem 8711.

04

Jak wybrać jeden wiersz pozycji, w którym ma stanąć kolumna z numerami?

Użyj ROW_NUMBER() OVER (PARTITION BY id_pozycji ORDER BY nr_palety, id_wiersza) i wstaw numery tylko tam, gdzie wynik to 1. Dodatkowa kolumna w ORDER BY rozstrzyga remis przy dwóch wierszach na tej samej palecie. Porównanie z MIN(nr_palety) powiela numery w obu takich wierszach.

05

Jak STRING_AGG traktuje NULL i pusty tekst?

NULL jest pomijany, a same wartości NULL dają NULL. Pusty tekst liczy się jako element i sortuje się na początku, więc wynik zaczyna się od przecinka: „, SN-1, SN-2”. Pusty tekst warto wykluczyć warunkiem w WHERE.

Słownik pojęć

Słownik pojęć

Pojęcia T-SQL użyte we wzorcu raportu z numerami w jednej kolumnie.

SSTRING_AGG
Funkcja agregująca T-SQL, która skleja wartości z wielu wierszy w jeden tekst z podanym separatorem. Dostępna od SQL Server 2017. Wynik ma limit 8000 bajtów, dopóki składnik nie jest typu max.
WWITHIN GROUP
Klauzula STRING_AGG, która ustala kolejność elementów w sklejonym tekście przez ORDER BY. Wymaga poziomu zgodności 110 lub wyższego. Dwa STRING_AGG z różnymi porządkami w jednym zakresie kończą się błędem 8711.
RROW_NUMBER
Funkcja okna, która numeruje wiersze w obrębie partycji od 1 według zadanego sortowania. We wzorcu wskazuje pierwszy wiersz pozycji, w którym ma stanąć kolumna z numerami.
PPartycja
Podzbiór wierszy wydzielony klauzulą PARTITION BY w funkcji okna. W przykładzie partycją jest pozycja dokumentu, więc numeracja zaczyna się od 1 w każdej pozycji osobno.
TTyp max
Typy varchar(max) i nvarchar(max) bez limitu 8000 bajtów. Rzutowanie składnika STRING_AGG na typ max usuwa błąd 9829 dla długich wyników.
ZZnacznik braku numerów
Wiersz typu BRAK w tabeli numerów, który w przykładzie oznacza pozycję bez numerów. Nie wchodzi do sklejonej kolumny, a jego obecność powoduje dopisanie adnotacji w pierwszym wierszu pozycji.

Zobacz to w praktyce

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