Cache the result of converting now() to a struct pg_tm.
authorTom Lane <tgl@sss.pgh.pa.us>
Mon, 28 Sep 2020 16:05:03 +0000 (12:05 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Mon, 28 Sep 2020 16:05:03 +0000 (12:05 -0400)
SQL operations such as CURRENT_DATE, CURRENT_TIME, LOCALTIME, and
conversion of "now" in a datetime input string have to obtain the
transaction start timestamp ("now()") as a broken-down struct pg_tm.
This is a remarkably expensive conversion, and since now() does not
change intra-transaction, it doesn't really need to be done more than
once per transaction.  Introducing a simple cache provides visible
speedups in queries that compute these values many times, for example
insertion of many rows that use a default value of CURRENT_DATE.

Peter Smith, with a bit of kibitzing by me

Discussion: https://postgr.es/m/CAHut+Pu89TWjq530V2gY5O6SWi=OEJMQ_VHMt8bdZB_9JFna5A@mail.gmail.com

src/backend/utils/adt/date.c
src/backend/utils/adt/datetime.c

index eaaffa7137dc7919d89983d09830752934e2ece5..057051fa85506cc66ad195515a2f2d6d510cd837 100644 (file)
@@ -299,20 +299,31 @@ EncodeSpecialDate(DateADT dt, char *str)
 DateADT
 GetSQLCurrentDate(void)
 {
-       TimestampTz ts;
-       struct pg_tm tt,
-                          *tm = &tt;
-       fsec_t          fsec;
-       int                     tz;
+       struct pg_tm tm;
 
-       ts = GetCurrentTransactionStartTimestamp();
+       static int      cache_year = 0;
+       static int      cache_mon = 0;
+       static int      cache_mday = 0;
+       static DateADT cache_date;
 
-       if (timestamp2tm(ts, &tz, tm, &fsec, NULL, NULL) != 0)
-               ereport(ERROR,
-                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                                errmsg("timestamp out of range")));
+       GetCurrentDateTime(&tm);
 
-       return date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - POSTGRES_EPOCH_JDATE;
+       /*
+        * date2j involves several integer divisions; moreover, unless our session
+        * lives across local midnight, we don't really have to do it more than
+        * once.  So it seems worth having a separate cache here.
+        */
+       if (tm.tm_year != cache_year ||
+               tm.tm_mon != cache_mon ||
+               tm.tm_mday != cache_mday)
+       {
+               cache_date = date2j(tm.tm_year, tm.tm_mon, tm.tm_mday) - POSTGRES_EPOCH_JDATE;
+               cache_year = tm.tm_year;
+               cache_mon = tm.tm_mon;
+               cache_mday = tm.tm_mday;
+       }
+
+       return cache_date;
 }
 
 /*
@@ -322,18 +333,12 @@ TimeTzADT *
 GetSQLCurrentTime(int32 typmod)
 {
        TimeTzADT  *result;
-       TimestampTz ts;
        struct pg_tm tt,
                           *tm = &tt;
        fsec_t          fsec;
        int                     tz;
 
-       ts = GetCurrentTransactionStartTimestamp();
-
-       if (timestamp2tm(ts, &tz, tm, &fsec, NULL, NULL) != 0)
-               ereport(ERROR,
-                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                                errmsg("timestamp out of range")));
+       GetCurrentTimeUsec(tm, &fsec, &tz);
 
        result = (TimeTzADT *) palloc(sizeof(TimeTzADT));
        tm2timetz(tm, fsec, tz, result);
@@ -348,18 +353,12 @@ TimeADT
 GetSQLLocalTime(int32 typmod)
 {
        TimeADT         result;
-       TimestampTz ts;
        struct pg_tm tt,
                           *tm = &tt;
        fsec_t          fsec;
        int                     tz;
 
-       ts = GetCurrentTransactionStartTimestamp();
-
-       if (timestamp2tm(ts, &tz, tm, &fsec, NULL, NULL) != 0)
-               ereport(ERROR,
-                               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                                errmsg("timestamp out of range")));
+       GetCurrentTimeUsec(tm, &fsec, &tz);
 
        tm2time(tm, fsec, &result);
        AdjustTimeForTypmod(&result, typmod);
index dec2fad82a685030f0df9c2fb8a66576ae49c000..91fab8cc9cb39257579e6d4cdd8f55fb84384216 100644 (file)
@@ -339,35 +339,80 @@ j2day(int date)
 /*
  * GetCurrentDateTime()
  *
- * Get the transaction start time ("now()") broken down as a struct pg_tm.
+ * Get the transaction start time ("now()") broken down as a struct pg_tm,
+ * converted according to the session timezone setting.
+ *
+ * This is just a convenience wrapper for GetCurrentTimeUsec, to cover the
+ * case where caller doesn't need either fractional seconds or tz offset.
  */
 void
 GetCurrentDateTime(struct pg_tm *tm)
 {
-       int                     tz;
        fsec_t          fsec;
 
-       timestamp2tm(GetCurrentTransactionStartTimestamp(), &tz, tm, &fsec,
-                                NULL, NULL);
-       /* Note: don't pass NULL tzp to timestamp2tm; affects behavior */
+       GetCurrentTimeUsec(tm, &fsec, NULL);
 }
 
 /*
  * GetCurrentTimeUsec()
  *
  * Get the transaction start time ("now()") broken down as a struct pg_tm,
- * including fractional seconds and timezone offset.
+ * including fractional seconds and timezone offset.  The time is converted
+ * according to the session timezone setting.
+ *
+ * Callers may pass tzp = NULL if they don't need the offset, but this does
+ * not affect the conversion behavior (unlike timestamp2tm()).
+ *
+ * Internally, we cache the result, since this could be called many times
+ * in a transaction, within which now() doesn't change.
  */
 void
 GetCurrentTimeUsec(struct pg_tm *tm, fsec_t *fsec, int *tzp)
 {
-       int                     tz;
+       TimestampTz cur_ts = GetCurrentTransactionStartTimestamp();
+
+       /*
+        * The cache key must include both current time and current timezone.  By
+        * representing the timezone by just a pointer, we're assuming that
+        * distinct timezone settings could never have the same pointer value.
+        * This is true by virtue of the hashtable used inside pg_tzset();
+        * however, it might need another look if we ever allow entries in that
+        * hash to be recycled.
+        */
+       static TimestampTz cache_ts = 0;
+       static pg_tz *cache_timezone = NULL;
+       static struct pg_tm cache_tm;
+       static fsec_t cache_fsec;
+       static int      cache_tz;
+
+       if (cur_ts != cache_ts || session_timezone != cache_timezone)
+       {
+               /*
+                * Make sure cache is marked invalid in case of error after partial
+                * update within timestamp2tm.
+                */
+               cache_timezone = NULL;
+
+               /*
+                * Perform the computation, storing results into cache.  We do not
+                * really expect any error here, since current time surely ought to be
+                * within range, but check just for sanity's sake.
+                */
+               if (timestamp2tm(cur_ts, &cache_tz, &cache_tm, &cache_fsec,
+                                                NULL, session_timezone) != 0)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+                                        errmsg("timestamp out of range")));
+
+               /* OK, so mark the cache valid. */
+               cache_ts = cur_ts;
+               cache_timezone = session_timezone;
+       }
 
-       timestamp2tm(GetCurrentTransactionStartTimestamp(), &tz, tm, fsec,
-                                NULL, NULL);
-       /* Note: don't pass NULL tzp to timestamp2tm; affects behavior */
+       *tm = cache_tm;
+       *fsec = cache_fsec;
        if (tzp != NULL)
-               *tzp = tz;
+               *tzp = cache_tz;
 }