PostgreSQL zawiera kilka wbudowanych typów danych związanych z datą i czasem. Dlaczego powinieneś używać ich zamiast ciągów lub liczb całkowitych? Na co należy uważać podczas ich używania? Przeczytaj, aby dowiedzieć się więcej o tym, jak efektywnie pracować z tymi typami danych w Postgresie.
Wiele typów
Standard SQL, standard ISO 8601, wbudowany katalog PostgreSQL i kompatybilność wsteczna razem definiują mnóstwo nakładających się, konfigurowalnych typów danych i konwencji związanych z datą/czasem, co jest w najlepszym razie mylące. To zamieszanie zwykle przenosi się na kod sterownika bazy danych, kod aplikacji, procedury SQL i skutkuje subtelnymi błędami, które są trudne do debugowania.
Z drugiej strony, używanie natywnych typów wbudowanych upraszcza instrukcje SQL i czyni je znacznie łatwiejszymi do czytania i pisania, a co za tym idzie, mniej podatne na błędy. Używanie, powiedzmy liczb całkowitych (liczba sekund od epoki) do reprezentowania czasu, skutkuje nieporęcznymi wyrażeniami SQL i więcej kodu aplikacji.
Zalety typów natywnych sprawiają, że warto zdefiniować zestaw niezbyt bolesnych reguł i egzekwować je w całej bazie kodu aplikacji i operacji. Oto jeden taki zestaw, który powinien zapewniać rozsądne wartości domyślne i rozsądny punkt wyjścia do dalszego dostosowywania, jeśli jest to wymagane.
Typy
Użyj tylko następujących 3 typów (chociaż wiele jest dostępnych):
- data - konkretna data, bez godziny
- sygnatura czasowa - konkretna data i godzina z rozdzielczością mikrosekundową
- przedział - interwał czasowy z rozdzielczością mikrosekundową
Te trzy typy razem powinny obsługiwać większość przypadków użycia aplikacji. Jeśli nie masz konkretnych potrzeb (takich jak oszczędzanie pamięci), zdecydowanie zalecamy trzymanie się tylko tych typów.
data reprezentuje datę bez godziny i jest bardzo przydatne w praktyce (patrz przykłady poniżej). Typ znacznika czasu to wariant, który zawiera informacje o strefie czasowej – bez informacji o strefie czasowej jest po prostu zbyt wiele zmiennych, które mogą wpływać na interpretację i wyodrębnianie wartości. Na koniec, przedział reprezentuje przedziały czasu od mikrosekundy do milionów lat.
Ciągi literalne
Używaj tylko następujących reprezentacji dosłownych i używaj operatora rzutowania, aby zmniejszyć szczegółowość bez poświęcania czytelności:
'2012-12-25'::date
-ISO 8601'2012-12-25 13:04:05.123-08:00'::timestamptz
-ISO 8601'1 month 3 days'::interval
- Postgres tradycyjny format wprowadzania interwałów
Pominięcie strefy czasowej pozostawia cię na łasce ustawienia strefy czasowej serwera Postgres, konfiguracji strefy czasowej, którą można ustawić na poziomie bazy danych, sesji, roli lub w parametrach połączenia, ustawienia strefy czasowej komputera klienckiego i więcej takich czynników.
Podczas wykonywania zapytań z kodu aplikacji, przekonwertuj typy interwałów na odpowiednią jednostkę (np. dni lub sekundy) za pomocą extract
funkcji i wczytaj wartość jako liczbę całkowitą lub rzeczywistą.
Konfiguracja i inne ustawienia
- Nie zmieniaj domyślnych ustawień konfiguracji GUC
DateStyle
,TimeZone
ilc_time
. - Nie ustawiaj ani nie używaj zmiennych środowiskowych
PGDATESTYLE
iPGTZ
. - Nie używaj
SET [SESSION|LOCAL] TIME ZONE ...
. - Jeśli możesz, ustaw strefę czasową systemu na UTC na maszynie, na której działa serwer Postgres, jak również na wszystkich maszynach z uruchomionym kodem aplikacji, który się z nim łączy.
- Sprawdź, czy sterownik bazy danych (np. złącze JDBC lub sterownik Godatabase/SQL) zachowuje się rozsądnie, gdy klient działa w jednej strefie czasowej, a serwer w innej. Upewnij się, że działa poprawnie, gdy
TimeZone
validnon-UTC parametr jest zawarty w ciągu połączenia.
Na koniec pamiętaj, że są to tylko wskazówki i można je dostosować do własnych potrzeb – ale upewnij się, że najpierw zbadasz implikacje takiego postępowania.
Typy i operatory natywne
Jak więc dokładnie używanie typów natywnych pomaga w uproszczeniu kodu SQL? Oto kilka przykładów.
Typ daty
Wartości daty typ można odjąć, aby uzyskać przedział między nimi. Możesz również dodać całkowitą liczbę dni do daty cząstek stałych lub dodać interwał do daty, aby uzyskać sygnaturę czasową :
-- 10 days from now (outputs 2020-07-26)
SELECT now()::date + 10;
-- 10 days from now (outputs 2020-07-26 04:44:30.568847+00)
SELECT now() + '10 days'::interval;
-- days till christmas (outputs 161 days 14:06:26.759466)
SELECT '2020-12-25'::date - now();
-- the 10 longest courses
SELECT name, end_date - start_date AS duration
FROM courses
ORDER BY end_date - start_date DESC
LIMIT 10;
Wartości tych typów są porównywalne, dlatego ostatnie zapytanie można uporządkować według end_date - start_date
, który ma typ przedziału . Oto kolejny przykład:
-- certificates expiring within the next 7 days
SELECT name
FROM certificates
WHERE expiry_date BETWEEN now() AND now() + '7 days'::interval;
Typ znacznika czasu
Wartości typu timestamptz można również odjąć (aby dać przedział ), dodano (do przedziału dać kolejny sygnaturę czasową ) i porównane.
-- difference of timestamps gives an interval
SELECT password_last_modified - created_at AS password_age
FROM users;
-- can also use the age() function
SELECT age(password_last_modified, created_at) AS password_age
FROM users;
Skoro już o tym mowa, zwróć uwagę, że istnieją 3 różne wbudowane funkcje, które zwracają różne wartości „bieżącego znacznika czasu”. W rzeczywistości zwracają różne rzeczy:
-- transaction_timestamp() returns the timestampsz of the start of current transaction
-- outputs 2020-07-16 05:09:32.677409+00
SELECT transaction_timestamp();
-- statement_timestamp() returns the timestamptz of the start of the current statement
SELECT statement_timestamp();
-- clock_timestamp() returns the timestamptz of the system clock
SELECT clock_timestamp();
Istnieją również aliasy dla tych funkcji:
-- now() actually returns the start of the current transaction, which means it
-- does not change during the transaction
SELECT now(), transaction_timestamp();
-- transaction timestamp is also returned by these keyword-style constructs
SELECT CURRENT_DATE, CURRENT_TIMESTAMP, transaction_timestamp();
Typy interwałów
Wartości wpisane w odstępach czasu mogą być używane jako typy danych kolumn, mogą być ze sobą porównywane i mogą być dodawane (i odejmowane od) znaczników czasu i dat. Oto kilka przykładów:
-- interval-typed values can be stored and compared
SELECT num
FROM passports
WHERE valid_for > '10 years'::interval
ORDER BY valid_for DESC;
-- you can multiply them by numbers (outputs 4 years)
SELECT 4 * '1 year'::interval;
-- you can divide them by numbers (outputs 3 mons)
SELECT '1 year'::interval / 4;
-- you can add and subtract them (outputs 1 year 1 mon 6 days)
SELECT '1 year'::interval + '1.2 months'::interval;
Inne funkcje i konstrukcje
PostgreSQL zawiera również kilka przydatnych funkcji i konstrukcji, których można używać do manipulowania wartościami tego typu.
Wyodrębnij
Funkcja extract może być użyta do pobrania określonej części z podanej wartości, np. miesiąca z daty. Pełna lista części, które można wyodrębnić, jest udokumentowana tutaj. Oto kilka przydatnych i nieoczywistych przykładów:
-- years from an interval (outputs 2)
SELECT extract(YEARS FROM '1.5 years 6 months'::interval);
-- day of the week (0=Sun .. 6=Sat) from timestamp (outputs 4)
SELECT extract(DOW FROM now());
-- day of the week (1=Mon .. 7=Sun) from timestamp (outputs 4)
SELECT extract(ISODOW FROM now());
-- convert interval to seconds (outputs 86400)
SELECT extract(EPOCH FROM '1 day'::interval);
Ostatni przykład jest szczególnie przydatny w zapytaniach uruchamianych przez aplikacje, ponieważ aplikacje mogą łatwiej obsługiwać interwał jako wartość zmiennoprzecinkową liczby sekund/minut/dni/itd.
Konwersja strefy czasowej
Istnieje również przydatna funkcja do wyrażania sygnatury czasowej w innej strefie czasowej. Zazwyczaj robi się to w kodzie aplikacji – w ten sposób łatwiej jest to przetestować i zmniejsza zależność od bazy danych stref czasowych, do której będzie się odwoływał Postgresserver. Niemniej jednak czasami może się przydać:
-- convert timestamps to a different time zone
SELECT timezone('Europe/Helsinki', now());
-- same as before, but this one is a SQL standard
SELECT now() AT TIME ZONE 'Europe/Helsinki';
Konwertowanie do i z tekstu
Funkcja to_char
(docs) może konwertować daty, znaczniki czasu i interwały na tekst w oparciu o ciąg formatujący – odpowiednik klasycznej funkcji C strftime
w Postgresie .
-- outputs Thu, 16th July
SELECT to_char(now(), 'Dy, DDth Month');
-- outputs 01 06 00 12 00 00
SELECT to_char('1.5 years'::interval, 'YY MM DD HH MI SS');
Do konwersji z tekstu na daty użyj to_date
, a do konwersji tekstu na znaczniki czasu użyj to_timestamp
. Pamiętaj, że jeśli korzystasz z formularzy wymienionych na początku tego postu, możesz zamiast tego użyć operatorów rzutowania.
-- outputs 2000-12-25 15:42:50+00
SELECT to_timestamp('2000.12.25.15.42.50', 'YYYY.MM.DD.HH24.MI.SS');
-- outputs 2000-12-25
SELECT to_date('2000.12.25.15.42.50', 'YYYY.MM.DD');
Zobacz dokumentację, aby zapoznać się z pełną listą wzorców ciągów formatu.
Najlepiej używać tych funkcji w prostych przypadkach. W przypadku bardziej skomplikowanego analizowania lub formatowania lepiej polegać na kodzie aplikacji, który (prawdopodobnie) może być lepiej przetestowany jednostkowo.
Interfejs z kodem aplikacji
Czasami nie jest wygodne przekazywanie wartości daty/czasu/interwału do i z kodu aplikacji, zwłaszcza gdy używane są parametry powiązane. Na przykład zwykle wygodniej jest przekazać interwał jako liczbę całkowitą dni (lub godzin lub minut) niż w postaci ciągu. Łatwiej jest też odczytać interwał jako liczbę całkowitą/zmiennoprzecinkową liczbę dni (lub godzin, minut itp.).
make_interval
funkcja może być użyta do utworzenia wartości przedziału z całkowitej liczby wartości składników (zobacz dokumentację tutaj). to_timestamp
Funkcja, którą widzieliśmy wcześniej, ma inną formę, która może tworzyć wartość atimestamptz z czasu epoki Uniksa.
-- pass the interval as number of days from the application code
SELECT name FROM courses WHERE duration <= make_interval(days => $1);
-- pass timestamptz as unix epoch (number of seconds from 1-Jan-1970)
SELECT id FROM events WHERE logged_at >= to_timestamp($1);
-- return interval as number of days (with a fractional part)
SELECT extract(EPOCH FROM duration) / 60 / 60 / 24;