- 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.
| DATEFIRST | DATEPART(weekday) dla środy | Wzór niezależny |
|---|---|---|
| 7 | 4 | 2 |
| 1 | 3 | 2 |
Przykład. Środa 2026-10-14, numer dnia liczony od poniedziałku jako 0.
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.
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.
| Wywołanie | Wynik |
|---|---|
| funkcja, odbiorca 1, środa 10:59:59, DATEFIRST 7 | 1 |
| funkcja, odbiorca 1, środa 10:59:59, DATEFIRST 1 | 1 |
| funkcja, odbiorca 1, środa 11:00:00, DATEFIRST 1 | 0 |
| procedura, odbiorca 1, środa 11:30:00 | błąd 50210 |
| procedura, odbiorca 99, środa 08:30:00 | błą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.