فصل ۲: SQL و انواع داده‌ی پستگرس

زمان درست: timestamptz، interval و منطقه‌ی زمانی تهران

همیشه لحظه را ذخیره کنید، نه ساعت دیواری را

پستگرس دو نوع زمان دارد: timestamp (بدون منطقه‌ی زمانی) و timestamptz (با منطقه‌ی زمانی). اسم دومی گمراه‌کننده است: timestamptz منطقه‌ی زمانی را ذخیره نمی‌کند؛ ورودی را به UTC تبدیل و ذخیره می‌کند و هنگام نمایش، با تنظیم TimeZone نشست به ساعت محلی برمی‌گرداند. یعنی یک لحظه‌ی مطلق در زمان. timestamp فقط «عددی روی ساعت دیواری» است که معلوم نیست مال کجاست.

SET TIME ZONE 'Asia/Tehran';
SELECT now();                                  -- 2026-09-28 14:30:00.123+03:30
SELECT now() AT TIME ZONE 'UTC';               -- timestamp بدون منطقه: 11:00
SELECT '2026-03-20 08:00'::timestamptz;         -- در منطقه‌ی نشست تفسیر می‌شود
SELECT '2026-03-20 08:00+03:30'::timestamptz = '2026-03-20 04:30Z'::timestamptz;  -- true

قاعده‌ی عملی: ستون‌های زمانی را timestamptz بگیرید، سرور را روی timezone = 'UTC' نگه دارید و در برنامه یا نشست، منطقه‌ی نمایش را تعیین کنید. جنگو با USE_TZ = True دقیقاً همین کار را می‌کند.

date، time و interval

SELECT current_date + 7;                          -- date + عدد صحیح = روز
SELECT now() - interval '90 minutes';
SELECT age(timestamptz '2026-09-28', timestamptz '1990-03-21');  -- 36 years 6 mons 7 days
SELECT extract(epoch FROM interval '2 hours');   -- 7200
SELECT date_trunc('month', now());               -- ابتدای ماه میلادی
SELECT date_bin('15 minutes', now(), timestamptz '2026-01-01');  -- گرد کردن به بازه‌ی ۱۵ دقیقه‌ای

تاریخ شمسی

پستگرس تقویم جلالی داخلی ندارد. روش استاندارد: همیشه میلادی (timestamptz) ذخیره کنید و تبدیل به شمسی را در لایه‌ی نمایش (مثلاً jdatetime در پایتون) انجام دهید. برای گزارش ماهانه‌ی شمسی، مرزهای ماه را در برنامه حساب کنید و به کوئری بدهید:

-- مهر ۱۴۰۵ = 2026-09-23 تا 2026-10-23 (نیمه‌باز)
SELECT count(*), sum(total_rial)
FROM orders
WHERE created_at >= timestamptz '2026-09-23 00:00+03:30'
  AND created_at <  timestamptz '2026-10-23 00:00+03:30';

اگر گزارش‌ها زیاد است، یک جدول تقویم (calendar) با ستون‌های تاریخ میلادی، سال و ماه و روز شمسی و تعطیلی بسازید و با آن JOIN کنید؛ یک بار پر می‌شود و همه‌ی گزارش‌ها ساده می‌شوند.

نکته‌هایی که کمتر کسی می‌داند

  • now() زمان شروع تراکنش است و در تمام تراکنش ثابت می‌ماند؛ برای زمان واقعی لحظه (مثلاً اندازه‌گیری مدت یک حلقه) از clock_timestamp() استفاده کنید.
  • ایران از سال ۲۰۲۳ ساعت تابستانی ندارد. اگر سروری tzdata قدیمی داشته باشد، ساعت‌های تابستان را با +04:30 نشان می‌دهد؛ بسته‌ی tzdata سیستم‌عامل را به‌روز کنید (بسته‌های دبیان و اوبونتو از tzdata سیستم استفاده می‌کنند).
  • برای بازه‌ی زمانی از BETWEEN استفاده نکنید؛ BETWEEN '2026-09-01' AND '2026-09-30' تقریباً کل روز آخر را جا می‌اندازد. همیشه >= شروع AND < شروعِ بعدی.
  • جمع کردن ماه با تاریخ آخر ماه عجیب است: date '2026-01-31' + interval '1 month' می‌شود 2026-02-28. برای سررسید اقساط، از روز اول ماه حساب کنید.
  • AT TIME ZONE جهت را عوض می‌کند: روی timestamptz یک timestamp محلی می‌دهد و روی timestamp یک timestamptz. دوبار پشت‌سرهم زدنش منشأ خطای رایج ۳.۵ ساعته است.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.