Cache the result of converting now() to a struct pg_tm.
authorTom Lane <[email protected]>
Mon, 28 Sep 2020 16:05:03 +0000 (12:05 -0400)
committerTom Lane <[email protected]>
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;
 }