diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 07f0385d80d4eeb4f568e43efbf387f4791a3a56..dea5195786a45ff18bb3d327d35ccb7c5b4cb050 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -2419,8 +2419,11 @@ January 8 04:05:06 1999 PST
         optional daylight-savings zone abbreviation, assumed to stand for one
         hour ahead of the given offset. For example, if <literal>EST5EDT</>
         were not already a recognized zone name, it would be accepted and would
-        be functionally equivalent to United States East Coast time.  When a
-        daylight-savings zone name is present, it is assumed to be used
+        be functionally equivalent to United States East Coast time.  In this
+        syntax, a zone abbreviation can be a string of letters, or an
+        arbitrary string surrounded by angle brackets (<literal>&lt;&gt;</>).
+        When a daylight-savings zone abbreviation is present,
+        it is assumed to be used
         according to the same daylight-savings transition rules used in the
         <literal>zoneinfo</> time zone database's <filename>posixrules</> entry.
         In a standard <productname>PostgreSQL</productname> installation,
diff --git a/doc/src/sgml/ref/set.sgml b/doc/src/sgml/ref/set.sgml
index d108dd4831c56757ccabd394406c2ec976a0fa6b..6290c9de70852cb6cb502b9e4804e456fe7c9973 100644
--- a/doc/src/sgml/ref/set.sgml
+++ b/doc/src/sgml/ref/set.sgml
@@ -243,7 +243,16 @@ SELECT setseed(<replaceable>value</replaceable>);
          </listitem>
         </varlistentry>
        </variablelist>
+      </para>
 
+      <para>
+       Timezone settings given as numbers or intervals are internally
+       translated to POSIX timezone syntax.  For example, after
+       <literal>SET TIME ZONE -7</>, <command>SHOW TIME ZONE</> would
+       report <literal>&lt;-07&gt;+07</>.
+      </para>
+
+      <para>
        See <xref linkend="datatype-timezones"> for more information
        about time zones.
       </para>
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index b6af6e7e2531300a6ccb887465fc3a26a982eb56..e1ce6ff5b0a45ab008a358933fd3298b460310ec 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -243,34 +243,17 @@ assign_datestyle(const char *newval, void *extra)
  * TIMEZONE
  */
 
-typedef struct
-{
-	pg_tz	   *session_timezone;
-	int			CTimeZone;
-	bool		HasCTZSet;
-} timezone_extra;
-
 /*
  * check_timezone: GUC check_hook for timezone
  */
 bool
 check_timezone(char **newval, void **extra, GucSource source)
 {
-	timezone_extra myextra;
+	pg_tz	   *new_tz;
+	long		gmtoffset;
 	char	   *endptr;
 	double		hours;
 
-	/*
-	 * Initialize the "extra" struct that will be passed to assign_timezone.
-	 * We don't want to change any of the three global variables except as
-	 * specified by logic below.  To avoid leaking memory during failure
-	 * returns, we set up the struct contents in a local variable, and only
-	 * copy it to *extra at the end.
-	 */
-	myextra.session_timezone = session_timezone;
-	myextra.CTimeZone = CTimeZone;
-	myextra.HasCTZSet = HasCTZSet;
-
 	if (pg_strncasecmp(*newval, "interval", 8) == 0)
 	{
 		/*
@@ -323,12 +306,11 @@ check_timezone(char **newval, void **extra, GucSource source)
 
 		/* Here we change from SQL to Unix sign convention */
 #ifdef HAVE_INT64_TIMESTAMP
-		myextra.CTimeZone = -(interval->time / USECS_PER_SEC);
+		gmtoffset = -(interval->time / USECS_PER_SEC);
 #else
-		myextra.CTimeZone = -interval->time;
+		gmtoffset = -interval->time;
 #endif
-		myextra.session_timezone = pg_tzset_offset(myextra.CTimeZone);
-		myextra.HasCTZSet = true;
+		new_tz = pg_tzset_offset(gmtoffset);
 
 		pfree(interval);
 	}
@@ -341,17 +323,14 @@ check_timezone(char **newval, void **extra, GucSource source)
 		if (endptr != *newval && *endptr == '\0')
 		{
 			/* Here we change from SQL to Unix sign convention */
-			myextra.CTimeZone = -hours * SECS_PER_HOUR;
-			myextra.session_timezone = pg_tzset_offset(myextra.CTimeZone);
-			myextra.HasCTZSet = true;
+			gmtoffset = -hours * SECS_PER_HOUR;
+			new_tz = pg_tzset_offset(gmtoffset);
 		}
 		else
 		{
 			/*
 			 * Otherwise assume it is a timezone name, and try to load it.
 			 */
-			pg_tz	   *new_tz;
-
 			new_tz = pg_tzset(*newval);
 
 			if (!new_tz)
@@ -367,40 +346,16 @@ check_timezone(char **newval, void **extra, GucSource source)
 				GUC_check_errdetail("PostgreSQL does not support leap seconds.");
 				return false;
 			}
-
-			myextra.session_timezone = new_tz;
-			myextra.HasCTZSet = false;
 		}
 	}
 
-	/*
-	 * Prepare the canonical string to return.	GUC wants it malloc'd.
-	 *
-	 * Note: the result string should be something that we'd accept as input.
-	 * We use the numeric format for interval cases, because it's simpler to
-	 * reload.	In the named-timezone case, *newval is already OK and need not
-	 * be changed; it might not have the canonical casing, but that's taken
-	 * care of by show_timezone.
-	 */
-	if (myextra.HasCTZSet)
-	{
-		char	   *result = (char *) malloc(64);
-
-		if (!result)
-			return false;
-		snprintf(result, 64, "%.5f",
-				 (double) (-myextra.CTimeZone) / (double) SECS_PER_HOUR);
-		free(*newval);
-		*newval = result;
-	}
-
 	/*
 	 * Pass back data for assign_timezone to use
 	 */
-	*extra = malloc(sizeof(timezone_extra));
+	*extra = malloc(sizeof(pg_tz *));
 	if (!*extra)
 		return false;
-	memcpy(*extra, &myextra, sizeof(timezone_extra));
+	*((pg_tz **) *extra) = new_tz;
 
 	return true;
 }
@@ -411,43 +366,19 @@ check_timezone(char **newval, void **extra, GucSource source)
 void
 assign_timezone(const char *newval, void *extra)
 {
-	timezone_extra *myextra = (timezone_extra *) extra;
-
-	session_timezone = myextra->session_timezone;
-	CTimeZone = myextra->CTimeZone;
-	HasCTZSet = myextra->HasCTZSet;
+	session_timezone = *((pg_tz **) extra);
 }
 
 /*
  * show_timezone: GUC show_hook for timezone
- *
- * We wouldn't need this, except that historically interval values have been
- * shown without an INTERVAL prefix, so the display format isn't what would
- * be accepted as input.  Otherwise we could have check_timezone return the
- * preferred string to begin with.
  */
 const char *
 show_timezone(void)
 {
 	const char *tzn;
 
-	if (HasCTZSet)
-	{
-		Interval	interval;
-
-		interval.month = 0;
-		interval.day = 0;
-#ifdef HAVE_INT64_TIMESTAMP
-		interval.time = -(CTimeZone * USECS_PER_SEC);
-#else
-		interval.time = -CTimeZone;
-#endif
-
-		tzn = DatumGetCString(DirectFunctionCall1(interval_out,
-											  IntervalPGetDatum(&interval)));
-	}
-	else
-		tzn = pg_get_timezone_name(session_timezone);
+	/* Always show the zone's canonical name */
+	tzn = pg_get_timezone_name(session_timezone);
 
 	if (tzn != NULL)
 		return tzn;
@@ -497,7 +428,7 @@ check_log_timezone(char **newval, void **extra, GucSource source)
 	*extra = malloc(sizeof(pg_tz *));
 	if (!*extra)
 		return false;
-	memcpy(*extra, &new_tz, sizeof(pg_tz *));
+	*((pg_tz **) *extra) = new_tz;
 
 	return true;
 }
@@ -519,6 +450,7 @@ show_log_timezone(void)
 {
 	const char *tzn;
 
+	/* Always show the zone's canonical name */
 	tzn = pg_get_timezone_name(log_timezone);
 
 	if (tzn != NULL)
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 33efb3c3cca933f69aa16286aa8c0e68d67bb754..e76704f315334e9be0f3cae5af06de41d0e5829b 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -93,8 +93,6 @@ bool		ExitOnAnyError = false;
 int			DateStyle = USE_ISO_DATES;
 int			DateOrder = DATEORDER_MDY;
 int			IntervalStyle = INTSTYLE_POSTGRES;
-bool		HasCTZSet = false;
-int			CTimeZone = 0;
 
 bool		enableFsync = true;
 bool		allowSystemTableMods = false;
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0aa540a08ef8e53db4e1aa339df27a20c4de66b6..98ca5539a4ca7e1a7f2ee0cf544f5eecdbce4be1 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -219,15 +219,6 @@ extern int	DateOrder;
 
 extern int	IntervalStyle;
 
-/*
- * HasCTZSet is true if user has set timezone as a numeric offset from UTC.
- * If so, CTimeZone is the timezone offset in seconds (using the Unix-ish
- * sign convention, ie, positive offset is west of UTC, rather than the
- * SQL-ish convention that positive is east of UTC).
- */
-extern bool HasCTZSet;
-extern int	CTimeZone;
-
 #define MAXTZLEN		10		/* max TZ name len, not counting tr. null */
 
 extern bool enableFsync;
diff --git a/src/test/regress/expected/horology.out b/src/test/regress/expected/horology.out
index 3ed9c8c1a095e939ef8cdd1c050fac307a96cdc5..87a695144eaf4f2ea1457dabefb8074e2182a5a9 100644
--- a/src/test/regress/expected/horology.out
+++ b/src/test/regress/expected/horology.out
@@ -2941,9 +2941,9 @@ DETAIL:  Value must be in the range -2147483648 to 2147483647.
 SET TIME ZONE 'America/New_York';
 SET TIME ZONE '-1.5';
 SHOW TIME ZONE;
-       TimeZone       
-----------------------
- @ 1 hour 30 mins ago
+    TimeZone    
+----------------
+ <-01:30>+01:30
 (1 row)
 
 SELECT '2012-12-12 12:00'::timestamptz;