PostgreSQL идва с куп вградени типове данни, свързани с дата и час. Защо трябва да ги използвате над низове или цели числа? За какво трябва да внимавате, докато ги използвате? Прочетете, за да научите повече за това как да работите ефективно с тези типове данни в Postgres.
Изцяло много видове
Стандартът SQL, стандартът ISO 8601, вграденият каталог на PostgreSQL и обратната съвместимост заедно определят множество припокриващи се, персонализирани типове данни, свързани с дата/време, и конвенции, които в най-добрия случай са объркващи. Това объркване обикновено се прелива в кода на драйвера на базата данни, кода на приложението, SQL рутинните процедури и води до фини грешки, които са трудни за отстраняване на грешки.
От друга страна, използването на естествени вградени типове опростява SQL изразите и ги прави много по-лесни за четене и писане и следователно по-малко податливи на грешки. Използването, да речем, цели числа (брой секунди от епохата) за представяне на времето, води до тромави SQL изрази и още код на приложението.
Предимствата на родните типове си струва да се дефинира набор от не толкова болезнени правила и да се прилагат в цялата база кодове на приложението и операциите. Ето един такъв набор, който трябва да осигури разумни настройки по подразбиране и разумна отправна точка за по-нататъшно персонализиране, ако е необходимо.
Типове
Използвайте само следните 3 типа (въпреки че много са налични):
- дата - конкретна дата, без час
- timestamptz - конкретна дата и час с микросекундна разделителна способност
- интервал - интервал от време с микросекундна разделителна способност
Тези три типа заедно трябва да поддържат повечето случаи на използване на приложения. Ако нямате специфични нужди (като запазване на място за съхранение), силно се препоръчва да се придържате само към тези типове.
Дата представлява дата без време и е доста полезна на практика (вижте примерите по-долу). Типът клеймо за време е вариантът, който включва информацията за часовата зона – без информацията за часовата зона просто има твърде много променливи, които могат да повлияят на интерпретацията и извличането на стойността. И накрая, интервалът представлява интервали от време от микросекунда до милиони години.
Литерални низове
Използвайте само следните буквални представяния и използвайте оператора cast, за да намалите многословието, без да жертвате четливостта:
'2012-12-25'::date
- ISO 8601'2012-12-25 13:04:05.123-08:00'::timestamptz
- ISO 8601'1 month 3 days'::interval
- Традиционен формат Postgres за интервално въвеждане
Пропускането на часовата зона ви оставя на милостта на настройката за часовата зона на сървъра Postgres, конфигурацията на TimeZone, която може да бъде зададена на ниво база данни, ниво на сесия, ниво на роля или в низа за връзка, настройката на часовата зона на клиентската машина и още такива фактори.
Докато правите заявка от кода на приложението, преобразувайте типове интервали в подходяща единица (като дни или секунди) с помощта на extract
функция и прочете стойността като цяло число или реална стойност.
Конфигурация и други настройки
- Не променяйте настройките по подразбиране за конфигурацията на GUC
DateStyle
,TimeZone
иlc_time
. - Не задавайте и не използвайте променливите на средата
PGDATESTYLE
иPGTZ
. - Не използвайте
SET [SESSION|LOCAL] TIME ZONE ...
. - Ако можете, задайте системната часова зона на UTC на машината, която изпълнява сървъра на Postgres, както и всички машини, изпълняващи код на приложението, които се свързват с него.
- Проверете дали драйверът на вашата база данни (като JDBC конектор или Godatabase/sql драйвер) се държи разумно, докато клиентът работи в една часова зона, а сървърът в друга. Уверете се, че работи правилно, когато е валиден не-UTC
TimeZone
параметърът е включен в низа за връзка.
И накрая, имайте предвид, че всичко това са само насоки и могат да бъдат настроени според нуждите ви – но не забравяйте първо да проучите последиците от това.
Нативни типове и оператори
И така, как точно използването на естествени типове помага за опростяване на SQL код? Ето няколко примера.
Тип дата
Стойности на дата типът може да се извади, за да се даде интервал между тях. Можете също да добавите цял брой дни към дата на частици, или да добавите интервал към дата, за да дадете timestamptz :
-- 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;
Стойностите на тези типове са сравними, поради което бихте могли да поръчате последната заявка по end_date - start_date
, който има тип интервал . Ето още един пример:
-- certificates expiring within the next 7 days
SELECT name
FROM certificates
WHERE expiry_date BETWEEN now() AND now() + '7 days'::interval;
Тип клеймо за време
Стойности от тип timestamptz може също да се извади (за да се даде интервал ),добавено (към интервал за да дадете още един timestamptz ) и сравнени.
-- 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;
Докато сте по темата, имайте предвид, че има 3 различни вградени функции, които връщат различни стойности на „текуща дата за време“. Те всъщност връщат различни неща:
-- 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();
Има и псевдоними за тези функции:
-- 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();
Типове на интервали
Стойности, въведени от интервал, могат да се използват като типове данни в колони, могат да се сравняват една с друга и могат да се добавят (и изваждат от) времеви печати и дати. Ето няколко примера:
-- 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;
Други функции и конструкции
PostgreSQL също така идва с няколко полезни функции и конструкции, които могат да се използват за манипулиране на стойности от тези типове.
Извличане
Функцията за извличане може да се използва за извличане на определена част от дадена стойност, като месеца от дата. Пълният списък с части, които могат да бъдат извлечени, е документиран тук. Ето няколко полезни и неочевидни примера:
-- 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);
Последният пример е особено полезен при заявки, изпълнявани от приложения, тъй като може да бъде по-лесно за приложенията да обработват интервал като стойност с плаваща запетая на броя секунди/минути/дни/и т.н.
Преобразуване на часовата зона
Има и удобна функция за изразяване на timestamptz в друга часова зона. Обикновено това се прави в кода на приложението – така е по-лесно да се тества и намалява зависимостта от базата данни за часовата зона, към която ще се позовава Postgresserver. Независимо от това, понякога може да бъде полезно:
-- 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';
Преобразуване към и от текст
Функцията to_char
(docs) може да преобразува дати, времеви марки и интервали в текст въз основа на форматен низ – Postgres еквивалент на класическата функция C strftime
.
-- 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');
За конвертиране от текст в дати използвайте to_date
, а за преобразуване на текст в timestamps използвайте to_timestamp
. Имайте предвид, че ако използвате формулярите, изброени в началото на тази публикация, можете просто да използвате операторите за прехвърляне.
-- 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');
Вижте документите за пълния списък с шаблони за форматни низове.
Най-добре е да използвате тези функции за прости случаи. За по-сложен синтактичен анализ или форматиране е по-добре да разчитате на код на приложението, който (вероятно) може да бъде по-добре тестван на модули.
Интерфейс с код на приложението
Понякога не е удобно да се предават стойности за дата/време/интервал към и от кода на приложението, особено когато се използват свързани параметри. Например, обикновено е по-удобно да се предава интервал като цяло число дни (или часове, или минути), а не във формат на низ. Освен това е по-лесно да се чете в интервал като цяло число/брой дни с плаваща запетая (или часове, или минути и т.н.).
make_interval
функцията може да се използва за създаване на стойност на интервал от интегрален брой стойности на компонентите (вижте документи тук). to_timestamp
функцията, която видяхме по-рано, има друга форма, която може да създаде стойност atimestamptz от времето на Unix епоха.
-- 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;