Allowed escaping # with backlash in identifier names (not in string), as it used to occur in database column names.
diff --git a/src/main/java/freemarker/core/_CoreStringUtils.java b/src/main/java/freemarker/core/_CoreStringUtils.java
index e545d7c..dc87b02 100644
--- a/src/main/java/freemarker/core/_CoreStringUtils.java
+++ b/src/main/java/freemarker/core/_CoreStringUtils.java
@@ -46,7 +46,8 @@
         scanForQuotationType: for (int i = 0; i < name.length(); i++) {
             final char c = name.charAt(i);
             if (!(i == 0 ? StringUtil.isFTLIdentifierStart(c) : StringUtil.isFTLIdentifierPart(c)) && c != '@') {
-                if ((quotationType == 0 || quotationType == '\\') && (c == '-' || c == '.' || c == ':')) {
+                if ((quotationType == 0 || quotationType == '\\')
+                        && StringUtil.isBackslashEscapedFTLIdentifierCharacter(c)) {
                     quotationType = '\\';
                 } else {
                     quotationType = '"';
@@ -66,8 +67,27 @@
         }
     }
 
-    private static String backslashEscapeIdentifier(String name) {
-        return StringUtil.replace(StringUtil.replace(StringUtil.replace(name, "-", "\\-"), ".", "\\."), ":", "\\:");
+    /*
+     * Escapes an identifier. This assumes that the identifier was once accepted by the parser, thus it is properly
+     * escapeable. Invalid characters that can't be escaped will be left as is. (This is actually feature because of
+     * historically weirdness, like that a sole {@code *} is a valid subvariable name, which must not be escaped.)
+     */
+    public static String backslashEscapeIdentifier(String name) {
+        StringBuilder sb = null;
+        for (int i = 0; i < name.length(); i++) {
+            char c = name.charAt(i);
+            if (StringUtil.isBackslashEscapedFTLIdentifierCharacter(c)) {
+                if (sb == null) {
+                    sb = new StringBuilder(name.length() + 8);
+                    sb.append(name, 0, i);
+                }
+                sb.append('\\');
+            }
+            if (sb != null) {
+                sb.append(c);
+            }
+        }
+        return sb == null ? name : sb.toString();
     }
 
     /**
diff --git a/src/main/java/freemarker/template/utility/StringUtil.java b/src/main/java/freemarker/template/utility/StringUtil.java
index 1238dd9..3317955 100644
--- a/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/src/main/java/freemarker/template/utility/StringUtil.java
@@ -1270,6 +1270,17 @@
     public static boolean isFTLIdentifierPart(final char c) {
         return isFTLIdentifierStart(c) || (c >= '0' && c <= '9');  
     }
+
+    /**
+     * Tells if a character can occur in an FTL identifier if it's preceded with a backslash. For example, {@code "-"}
+     * is a such character (as you can have an identifier like {@code foo\-bar} in FTL), but {@code "f"} is not, as
+     * it needn't be, and can't be escaped.
+     *
+     * @since 2.3.31
+     */
+    public static boolean isBackslashEscapedFTLIdentifierCharacter(final char c) {
+        return c == '-' || c == '.' || c == ':' || c ==  '#';
+    }
     
     /**
      * Escapes the <code>String</code> with the escaping rules of Java language
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index cb80218..a6ee15b 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -1544,7 +1544,8 @@
         ]
     >
     |
-    <#ESCAPED_ID_CHAR: "\\" ("-" | "." | ":")>
+    // Keep this in sync with StringUtil.isBackslashEscapedFTLIdentifierCharacter
+    <#ESCAPED_ID_CHAR: "\\" ("-" | "." | ":" | "#")>
     |
     <#ID_START_CHAR: <NON_ESCAPED_ID_START_CHAR>|<ESCAPED_ID_CHAR>>
     |
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 454979a..5e9a44f 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -2681,8 +2681,8 @@
             non-Latin digits), underline (<literal>_</literal>), dollar
             (<literal>$</literal>), at sign (<literal>@</literal>).
             Furthermore, the first character can't be an ASCII digit
-            (<literal>0</literal>-<literal>9</literal>). Starting from
-            FreeMarker 2.3.22, the variable name can also contain minus
+            (<literal>0</literal>-<literal>9</literal>). Since FreeMarker
+            2.3.22 the variable name can also contain minus
             (<literal>-</literal>), dot (<literal>.</literal>), and colon
             (<literal>:</literal>) at any position, but these must be escaped
             with a preceding backslash (<literal>\</literal>), otherwise they
@@ -2690,7 +2690,10 @@
             whose name is <quote>data-id</quote>, the expression is
             <literal>data\-id</literal>, as <literal>data-id</literal> would
             be interpreted as <quote>data minus id</quote>. (Note that these
-            escapes only work in identifiers, not in string literals.)</para>
+            escapes only work in identifiers, not in string literals.)
+            Furthermore, since FreeMarker 2.3.31, hash mark
+            (<literal>#</literal>) can also be used, but must be escaped with
+            a preceding backslash (<literal>\</literal>).</para>
           </section>
 
           <section xml:id="dgui_template_exp_var_hash">
@@ -29413,6 +29416,16 @@
             </listitem>
 
             <listitem>
+              <para>Allowed escaping <literal>#</literal> with backlash in
+              identifier names (not in string), as it used to occur in
+              database column names. Like if you have a column name like
+              <literal>#users</literal>, you can refer to it as
+              <literal>row.\#users</literal>. (Alternatively,
+              <literal>row['#users']</literal> always worked, but is often
+              less convenient.)</para>
+            </listitem>
+
+            <listitem>
               <para><link
               xlink:href="https://issues.apache.org/jira/projects/FREEMARKER/issues/FREEMARKER-169">FREEMARKER-169</link>:
               Fixed bug that made <literal>?c</literal> and
diff --git a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl
index 75d52f1..11b1172 100644
--- a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl
+++ b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl
@@ -30,9 +30,9 @@
 </#function>
 ${f\-a("f-a")}
 
-<#assign \-\-\-\.\: = 'dash-dash-dash etc.'>
-${\-\-\-\.\:}
-${.vars['---.:']}
+<#assign \-\-\-\.\:\# = 'dash-dash-dash etc.'>
+${\-\-\-\.\:\#}
+${.vars['---.:#']}
 <#assign hash = { '--moz-prop': 'propVal' }>
 ${hash.\-\-moz\-prop}
 ${hash['--moz-prop']}
diff --git a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out
index 17e2b4e..96eff6b 100644
--- a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out
+++ b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out
@@ -21,8 +21,8 @@
 
 <#function f\-a(p\-a)><#return p\-a + " works"/></#function>${f\-a("f-a")}
 
-<#assign \-\-\-\.\: = "dash-dash-dash etc.">${\-\-\-\.\:}
-${.vars["---.:"]}
+<#assign \-\-\-\.\:\# = "dash-dash-dash etc.">${\-\-\-\.\:\#}
+${.vars["---.:#"]}
 <#assign hash = {"--moz-prop": "propVal"}>${hash.\-\-moz\-prop}
 ${hash["--moz-prop"]}
 
diff --git a/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt b/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt
index 1c62bd5..5afbaec 100644
--- a/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt
+++ b/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt
@@ -40,7 +40,7 @@
 
 <catchAll x=1 y=2 a:b.c=5 data-foo=4 z=3 />
 
----.: = dash-dash-dash etc.
+---.:# = dash-dash-dash etc.
 @as@_a = as1
 as/b = as3
 as'c = as4
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl b/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl
index 9b39235..e29d997 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl
@@ -30,9 +30,9 @@
 </#function>
 ${f\-a("f-a")}
 
-<#assign \-\-\-\.\: = 'dash-dash-dash etc.'>
-${\-\-\-\.\:}
-${.vars['---.:']}
+<#assign \-\-\-\.\:\# = 'dash-dash-dash etc.'>
+${\-\-\-\.\:\#}
+${.vars['---.:#']}
 <#assign hash = { '--moz-prop': 'propVal' }>
 ${hash.\-\-moz\-prop}
 ${hash['--moz-prop']}