SQL Server w logistyce

Okno zamówień w T-SQL: dzień tygodnia i godzina graniczna niezależne od DATEFIRST

Numer dnia z DATEPART zależy od DATEFIRST i języka sesji, więc reguła okna zamówień zapisana w danych wymaga wzoru, który tych ustawień nie czyta. Godzinę graniczną porównujemy jako czas.

sql.server.net.pl/sql-server/
Uśmiechnięty mężczyzna przy monitorze z oknem kalendarza
Uśmiechnięty mężczyzna przy monitorze z oknem kalendarza
W skrócie
  • Reguła: okno zamówień (dzień tygodnia i godzina graniczna) leży w tabeli konfiguracji i jest sprawdzane w bazie funkcją oraz procedurą.
  • Numer dnia: wzór (DATEPART(weekday, d) + @@DATEFIRST - 2) % 7 daje 0 dla poniedziałku niezależnie od DATEFIRST.
  • Godzina: czas porównujemy ze ścisłą nierównością, więc o godzinie granicznej okno jest już zamknięte.
  • Błędy: THROW 50210 przy zamkniętym oknie i THROW 50211 przy braku konfiguracji odbiorcy.

Dzień tygodnia niezależny od DATEFIRST w T-SQL jest potrzebny wszędzie tam, gdzie reguła „kiedy odbiorca może składać zamówienie” leży w tabeli konfiguracji i bywa sprawdzana w bazie. Pułapka kryje się w numerze dnia: DATEPART(weekday) zwraca dla środy 4 albo 3, zależnie od ustawienia DATEFIRST. Odbiorca Testowy ma w przykładzie środę do 11:00, więc zapytanie, które pomyli dzień, odrzuci zamówienie albo przyjmie je o złej porze.

Numer dnia tygodnia niezależny od DATEFIRST daje wzór (DATEPART(weekday, d) + @@DATEFIRST - 2) % 7: dla poniedziałku zawsze 0, dla środy zawsze 2. Godzinę graniczną porównujemy jako czas, a nie jako godzinę, i to ścisłą nierównością. Całość mieści się w funkcji skalarnej i w procedurze, która rzuca THROW.

To wzorzec z nazwami przykładowymi. Sprawdziliśmy go na SQL Server 2022 przy czterech poziomach zgodności z zakresu od 110 do 160 i wynik był za każdym razem ten sam.

Reguła okna zamówień leży w tabeli konfiguracji

Tabela konfiguracji ma trzy pola, widoczne w bloku poniżej. Dzień to liczba typu tinyint z ograniczeniem CHECK od 0 do 6, gdzie 0 oznacza poniedziałek. Godzina graniczna ma typ time(0), a kluczem głównym jest para odbiorca i dzień.

Dane próbne są krótkie: odbiorca 1 ma środę do 11:00 i piątek do 08:30, a odbiorca 2 poniedziałek do 12:00.

CREATE TABLE dbo.OknoZamowien (
    IdOdbiorcy         int     NOT NULL,
    DzienTygodnia    tinyint NOT NULL CONSTRAINT CK_Okno_Dzien CHECK (DzienTygodnia BETWEEN 0 AND 6),
    GodzinaGraniczna time(0) NOT NULL,
    CONSTRAINT PK_OknoZamowien PRIMARY KEY (IdOdbiorcy, DzienTygodnia)
);
INSERT dbo.OknoZamowien VALUES (1, 2, '11:00'), (1, 4, '08:30'), (2, 0, '12:00');

Przykład ilustracyjny. Wartości są wymyślone, a numer dnia 0 oznacza poniedziałek.

Numer dnia z DATEPART zależy od DATEFIRST i języka sesji

Dla środy 2026-10-14 DATEPART(weekday, ...) zwraca 4 przy DATEFIRST 7 i 3 przy DATEFIRST 1. Wzór niezależny zwraca w obu przypadkach 2. Język sesji ustawia DATEFIRST: w świeżej sesji SET LANGUAGE polish dało 1, a SET LANGUAGE us_english dało 7. Jawne SET DATEFIRST 3 przetrwało obie zmiany języka, bo ustawienie jawne wygrywa. DATENAME(weekday, ...) zwróciło „środa” albo „Wednesday”, zależnie od języka.

Numer środy z DATEPART przy dwóch ustawieniach DATEFIRST
DATEFIRSTDATEPART(weekday) dla środyWzór niezależny
742
132

Przykład. Środa 2026-10-14, numer dnia liczony od poniedziałku jako 0.

Schemat sprawdzania okna zamówień niezależnie od DATEFIRSTSchemat: moment, numer dnia niezależny od DATEFIRST, odczyt wiersza konfiguracji i porównanie czasu z godziną graniczną.Okno zamówień: numer dnia nie zależy od DATEFIRSTJedna funkcja sprawdza dzień i godzinę graniczną, a procedura osobno rozpoznaje odbiorcę bez konfiguracji.Moment (parametralbo SYSDATETIME)Numer dnia 0-6 ze wzoruniezależnego od DATEFIRSTWiersz konfiguracji: odbiorca, dzień,godzina granicznaDATEPART(weekday) dla środy dałby 4albo 3, zależnie od DATEFIRST(DATEPART(weekday, d) +@@DATEFIRST - 2) % 7IdOdbiorcyDzienTygodniaGodzinaGraniczna1211:001408:30czas < godzinagraniczna?tak1 - okno otwartenie albo brak wiersza0 - okno zamknięte; procedura zatrzymujesię przez THROWPorównanie jest ścisłe: o godzinie granicznej okno jest już zamknięte.
Schemat: moment trafia do wzoru numeru dnia, potem do wiersza konfiguracji odbiorcy, a czas jest porównywany z godziną graniczną.

Numer dnia z DATEPART opisuje ustawienia sesji, a nie kalendarz, więc reguła zapisana w danych potrzebuje wzoru, który tych ustawień nie czyta.

Wzór z @@DATEFIRST daje ten sam numer dnia w każdej sesji

Wzór przelicza numer z bieżącego ustawienia sesji na numer liczony od poniedziałku i działa także wewnątrz funkcji. Ten sam moment, 2026-10-14 10:59:59, dał 1 zarówno przy DATEFIRST 7, jak i przy DATEFIRST 1.

Kontrola obejmuje 98 prób: siedem ustawień DATEFIRST razy 14 kolejnych dni. Wzór dał ten sam numer co DATEDIFF(day, '19000101', d) % 7, bo 1900-01-01 to poniedziałek, oraz oczekiwany numer dnia. Błędów było zero.

DECLARE @df int = 1, @bledy int = 0, @proby int = 0, @d date, @i int, @w int, @sql nvarchar(200);
WHILE @df <= 7
BEGIN
    SET @sql = N'SET DATEFIRST ' + CAST(@df AS nvarchar(1)) +
               N'; SELECT @w = (DATEPART(weekday, @d) + @@DATEFIRST - 2) % 7;';
    SET @i = 0;
    WHILE @i < 14
    BEGIN
        SET @d = DATEADD(day, @i, '2026-10-12');
        EXEC sp_executesql @sql, N'@d date, @w int OUTPUT', @d = @d, @w = @w OUTPUT;
        SET @proby += 1;
        IF @w <> DATEDIFF(day, '19000101', @d) % 7 OR @w <> @i % 7 SET @bledy += 1;
        SET @i += 1;
    END;
    SET @df += 1;
END;
SELECT @proby AS Prob, @bledy AS Bledy;

Przykład ilustracyjny. Kontrola 98 prób. Oczekiwany wynik: 98 prób i 0 błędów.

Godzinę graniczną porównujemy jako czas, a nie jako godzinę

Porównanie samej godziny, DATEPART(hour, '11:30') <= DATEPART(hour, '11:00'), dało 1, czyli okno otwarte do końca godziny granicznej. Porównanie czasu, '11:30' < '11:00', dało 0. Funkcja porównuje więc czas z momentu z godziną graniczną typu time ścisłą nierównością, a o godzinie granicznej okno jest już zamknięte.

Siedem przypadków brzegowych odbiorcy 1 pokazuje makieta. Środa 08:30 i 10:59:59 dają 1, a środa 11:00:00 i 11:30 dają 0. Czwartek 08:00 daje 0, piątek 08:29 daje 1, a niedziela 07:00 daje 0.

Wynik zapytania: siedem momentów odbiorcy 1 i wartość funkcji oknaMakieta: wynik zapytania z siedmioma momentami odbiorcy 1 i wartością 1 lub 0 w kolumnie Otwarte.Wyniki zapytań7 wierszyOkno otwarte dla odbiorcy 1 (środa do 11:00, piątek do 08:30)SELECT dbo.fnOknoOtwarte(1, '2026-10-14T10:59:59') AS Otwarte;   -- 1SELECT dbo.fnOknoOtwarte(1, '2026-10-14T11:00:00') AS Otwarte;   -- 0OpisTerazOtwarteśroda 08:302026-10-14 08:30:001środa 10:59:592026-10-14 10:59:591środa 11:00:002026-10-14 11:00:000środa 11:302026-10-14 11:30:000czwartek 08:002026-10-15 08:00:000piątek 08:292026-10-16 08:29:001niedziela 07:002026-10-18 07:00:000Przy DATEFIRST 7 i przy DATEFIRST 1 wynik jest ten sam.Makieta poglądowa, dane przykładowe
Makieta poglądowa, dane przykładowe. Siedem momentów odbiorcy 1 z wartością funkcji okna: jedynka w trzech wierszach, zero w czterech.

Funkcja zwraca bit, a procedura rzuca THROW z komunikatem

Funkcja skalarna fnOknoOtwarte(@IdOdbiorcy, @Teraz) zwraca bit. Daje 1, gdy dla odbiorcy i dnia istnieje wiersz konfiguracji, a czas z momentu jest mniejszy od godziny granicznej, a w przeciwnym razie ISNULL(..., 0). Odbiorca bez konfiguracji dostaje więc 0, czyli okno zamknięte. Moment bierzemy z typu datetime2(0).

CREATE FUNCTION dbo.fnOknoOtwarte (@IdOdbiorcy int, @Teraz datetime2(0))
RETURNS bit
AS
BEGIN
    RETURN ISNULL((
        SELECT 1
        FROM dbo.OknoZamowien AS o
        WHERE o.IdOdbiorcy = @IdOdbiorcy
          AND o.DzienTygodnia = (DATEPART(weekday, @Teraz) + @@DATEFIRST - 2) % 7
          AND CAST(@Teraz AS time(0)) < o.GodzinaGraniczna), 0);
END;
GO

Przykład ilustracyjny. Funkcja stoi w osobnej partii, bo CREATE FUNCTION musi być pierwszą instrukcją partii.

Procedura ZlozZamowienie(@IdOdbiorcy, @Teraz = NULL) przyjmuje zamówienie albo rzuca THROW 50210 z komunikatem „Zamówienia tego odbiorcy nie są teraz przyjmowane.”. Brak konfiguracji odbiorcy rozpoznaje osobny THROW 50211 z komunikatem „Odbiorca nie ma zdefiniowanego okna zamówień.”, sprawdzany przed oknem. Moment jest parametrem, domyślnie SYSDATETIME(), więc zachowanie sprawdzamy bez zmiany zegara.

CREATE PROCEDURE dbo.ZlozZamowienie @IdOdbiorcy int, @Teraz datetime2(0) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    SET @Teraz = ISNULL(@Teraz, SYSDATETIME());
    IF NOT EXISTS (SELECT 1 FROM dbo.OknoZamowien WHERE IdOdbiorcy = @IdOdbiorcy)
        THROW 50211, N'Odbiorca nie ma zdefiniowanego okna zamówień.', 1;
    IF dbo.fnOknoOtwarte(@IdOdbiorcy, @Teraz) = 0
        THROW 50210, N'Zamówienia tego odbiorcy nie są teraz przyjmowane.', 1;
    SELECT N'zamówienie przyjęte' AS Wynik, @IdOdbiorcy AS IdOdbiorcy, @Teraz AS Kiedy;
END;

Przykład ilustracyjny. Procedura w osobnej partii, po funkcji.

Parametr @Teraz ma typ datetime2(0), więc wartość domyślna SYSDATETIME() jest zaokrąglana do pełnej sekundy, a nie obcinana: 10:59:59,6 daje 11:00:00 i okno jest już zamknięte (błąd 50210). Wywołanie z jawnym momentem w pełnej sekundzie tego nie dotyczy. THROW poza blokiem TRY przerywa całą partię, więc kolejne wywołania z błędem uruchamiamy w osobnych partiach albo w TRY/CATCH. O błędach po ROLLBACK piszemy w artykule o TRY CATCH i THROW w procedurze zapisu.

SET DATEFIRST 7;
SELECT dbo.fnOknoOtwarte(1, '2026-10-14T10:59:59') AS Otwarte;
SET DATEFIRST 1;
SELECT dbo.fnOknoOtwarte(1, '2026-10-14T10:59:59') AS Otwarte;
SELECT dbo.fnOknoOtwarte(1, '2026-10-14T11:00:00') AS Otwarte;
GO
EXEC dbo.ZlozZamowienie 1, '2026-10-14T11:30:00';
GO
EXEC dbo.ZlozZamowienie 99, '2026-10-14T08:30:00';
GO

Przykład ilustracyjny. Trzy odczyty funkcji i dwa wywołania procedury, każde w osobnej partii.

Wyniki wywołań funkcji i procedury
WywołanieWynik
funkcja, odbiorca 1, środa 10:59:59, DATEFIRST 71
funkcja, odbiorca 1, środa 10:59:59, DATEFIRST 11
funkcja, odbiorca 1, środa 11:00:00, DATEFIRST 10
procedura, odbiorca 1, środa 11:30:00błąd 50210
procedura, odbiorca 99, środa 08:30:00błąd 50211

Przykład. Wyniki kodu z poprzedniego bloku.

W scenariuszu (przykład) Odbiorca Testowy ma w konfiguracji środę do 11:00. W środę o 10:59:59 procedura przyjmuje zamówienie, a o 11:00:00 zatrzymuje się komunikatem, że zamówienia nie są teraz przyjmowane. Ta sama procedura uruchomiona z sesji o innym DATEFIRST daje ten sam wynik, bo numer dnia liczy się ze wzoru, który to ustawienie uwzględnia. Dla odbiorcy bez wiersza w konfiguracji procedura rzuca osobny komunikat o braku zdefiniowanego okna, a moment jest parametrem, więc test nie wymaga czekania do środy.

Dzień zapisany jako tekst daje fałszywe porównania

To ogólne pułapki T-SQL, niezwiązane z żadnym systemem. Przy zapisanym numerze 1-7 liczonym od poniedziałku (środa to 3) zapytanie z warunkiem Dzien = DATEPART(weekday, ...) miało 0 trafień przy DATEFIRST 7 i 1 trafienie przy DATEFIRST 1.

Porównanie CAST(2 AS varchar(2)) = '02' daje fałsz, a CAST('02' AS int) = 2 prawdę. Pusty tekst zamienia się na liczbę 0 w CAST('' AS int) i w TRY_CONVERT(int, ''), czyli na poniedziałek. Dlatego dzień trzyma kolumna liczbowa z ograniczeniem CHECK.

Komunikat THROW i zegar serwera mają własne zasady

THROW nie przyjmuje wyrażenia jako komunikatu: złączenie tekstu z liczbą operatorem plus, na przykład THROW 50020, N'Odbiorca ' + ..., kończy się błędem składni 102 (Incorrect syntax near '+'). Komunikat składamy w zmiennej funkcją CONCAT, a do THROW przekazujemy zmienną.

SYSDATETIME() zwraca czas serwera. Czas lokalny odbiorcy daje AT TIME ZONE zastosowane do SYSDATETIMEOFFSET(), na przykład z nazwą strefy 'Central European Standard Time'. Wyrażenie wykonało się poprawnie, a serwer testowy działał w tej samej strefie, więc wynik się nie różnił.

Pytania przed napisaniem własnego okna zamówień

Zanim przeniesiesz wzorzec do własnej bazy, odpowiedz sobie na cztery pytania.

  • Sesje: w jakich językach i z jakim ustawieniem DATEFIRST łączą się z bazą aplikacje i użytkownicy, którzy wywołują procedury.
  • Granica: czy godzina graniczna ma obowiązywać włącznie, czy wyłącznie, i czy wystarczy dokładność do sekundy.
  • Zegar: czyj zegar jest wiążący: serwera bazy czy lokalny czas odbiorcy.
  • Brak konfiguracji: co ma się dziać, gdy odbiorca nie ma wiersza w konfiguracji: zamknięte okno czy komunikat o braku konfiguracji.

Pokrewne wzorce to alokacja wydania na dostawy oraz wybór partii z NULL w ORDER BY. Funkcję w bazie jako źródło prawdy o sesji opisuje artykuł o tożsamości użytkownika w SQL Server. Wszystkie artykuły sekcji zebraliśmy w spisie artykułów o SQL Server.

FAQ

Najczęstsze pytania o dzień tygodnia i DATEFIRST w T-SQL

01

Dlaczego DATEPART(weekday) zwraca różne numery dla tego samego dnia?

Numer dnia z DATEPART(weekday) zależy od ustawienia DATEFIRST, a to ustawia język sesji. Dla środy 2026-10-14 wynik to 4 przy DATEFIRST 7 i 3 przy DATEFIRST 1. W świeżej sesji SET LANGUAGE polish dało DATEFIRST 1, a us_english 7, natomiast jawne SET DATEFIRST 3 przetrwało obie zmiany języka.

02

Jak policzyć numer dnia tygodnia niezależnie od DATEFIRST?

Wzorem (DATEPART(weekday, d) + @@DATEFIRST - 2) % 7, który daje 0 dla poniedziałku. Dla środy zwrócił 2 zarówno przy DATEFIRST 7, jak i przy 1. W kontroli 98 prób, czyli siedem ustawień DATEFIRST razy 14 kolejnych dni, błędów było zero.

03

Czemu porównywać czas, a nie samą godzinę?

Porównanie samej godziny przez DATEPART(hour, ...) traktuje całą godzinę graniczną jako otwartą, więc 11:30 przy granicy 11:00 dało 1. Porównanie czasu dało 0. Funkcja porównuje czas z godziną graniczną typu time ścisłą nierównością, więc o godzinie granicznej okno jest już zamknięte.

04

Jak przekazać komunikat z wartością do THROW?

THROW nie przyjmuje wyrażenia jako komunikatu: złączenie tekstu z liczbą operatorem plus kończy się błędem składni 102. Komunikat składamy w zmiennej funkcją CONCAT i do THROW przekazujemy zmienną.

05

Jak przetestować regułę bez czekania na odpowiedni dzień i godzinę?

Moment jest parametrem procedury, domyślnie SYSDATETIME(), więc zachowanie sprawdzamy bez zmiany zegara. W przykładzie odbiorca 1 dostaje w środę 1 o 08:30 i 10:59:59 oraz 0 o 11:00:00 i 11:30. Parametr datetime2(0) zaokrągla moment do pełnej sekundy.

Słownik pojęć

Słownik pojęć

Pojęcia użyte we wzorcu okna zamówień w T-SQL.

DDATEFIRST
Ustawienie sesji wskazujące, który dzień tygodnia ma numer 1. Zależy od języka sesji i zmienia wynik DATEPART(weekday), a wzór z @@DATEFIRST usuwa tę zależność.
DDATEPART
Funkcja zwracająca wybraną część daty. DATEPART(weekday, d) zwraca numer dnia tygodnia liczony według DATEFIRST, więc ten sam dzień ma w różnych sesjach różne numery.
GGodzina graniczna
Czas, od którego okno zamówień jest zamknięte. We wzorcu ma typ time(0), a porównanie jest ścisłe: o godzinie granicznej okno już się nie otwiera.
TTHROW
Instrukcja zgłaszająca błąd o podanym numerze i komunikacie. Nie przyjmuje wyrażenia jako komunikatu, a poza blokiem TRY przerywa całą partię.
FFunkcja skalarna
Funkcja zwracająca jedną wartość, tu typu bit. We wzorcu daje 1, gdy okno jest otwarte, a w pozostałych przypadkach 0.
SStrefa czasowa
Obszar o wspólnym czasie lokalnym. SYSDATETIME() zwraca czas serwera, a czas lokalny odbiorcy daje SYSDATETIMEOFFSET() z klauzulą AT TIME ZONE i nazwą strefy.
SSYSDATETIME
Funkcja zwracająca bieżący czas serwera z dokładnością datetime2. Jest domyślnym momentem procedury, a typ parametru datetime2(0) zaokrągla go do pełnej sekundy.

Zobacz to w praktyce

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