diff --git a/django/contrib/postgres/search.py b/django/contrib/postgres/search.py
index f8c691d73b..08d92e3514 100644
--- a/django/contrib/postgres/search.py
+++ b/django/contrib/postgres/search.py
@@ -60,14 +60,6 @@ class SearchVector(SearchVectorCombinable, Func):
 
     def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
         resolved = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
-        resolved.set_source_expressions([
-            Coalesce(
-                expression
-                if isinstance(expression.output_field, (CharField, TextField))
-                else Cast(expression, TextField()),
-                Value('')
-            ) for expression in resolved.get_source_expressions()
-        ])
         if self.config:
             if not hasattr(self.config, 'resolve_expression'):
                 resolved.config = Value(self.config).resolve_expression(query, allow_joins, reuse, summarize, for_save)
@@ -76,17 +68,26 @@ class SearchVector(SearchVectorCombinable, Func):
         return resolved
 
     def as_sql(self, compiler, connection, function=None, template=None):
+        clone = self.copy()
+        clone.set_source_expressions([
+            Coalesce(
+                expression
+                if isinstance(expression.output_field, (CharField, TextField))
+                else Cast(expression, TextField()),
+                Value('')
+            ) for expression in clone.get_source_expressions()
+        ])
         config_params = []
         if template is None:
-            if self.config:
-                config_sql, config_params = compiler.compile(self.config)
+            if clone.config:
+                config_sql, config_params = compiler.compile(clone.config)
                 template = '%(function)s({}::regconfig, %(expressions)s)'.format(config_sql.replace('%', '%%'))
             else:
-                template = self.template
-        sql, params = super().as_sql(compiler, connection, function=function, template=template)
+                template = clone.template
+        sql, params = super(SearchVector, clone).as_sql(compiler, connection, function=function, template=template)
         extra_params = []
-        if self.weight:
-            weight_sql, extra_params = compiler.compile(self.weight)
+        if clone.weight:
+            weight_sql, extra_params = compiler.compile(clone.weight)
             sql = 'setweight({}, {})'.format(sql, weight_sql)
         return sql, config_params + params + extra_params
 
diff --git a/docs/releases/2.2.2.txt b/docs/releases/2.2.2.txt
index 377abaa3c0..d68338dce2 100644
--- a/docs/releases/2.2.2.txt
+++ b/docs/releases/2.2.2.txt
@@ -14,3 +14,7 @@ Bugfixes
 
 * Fixed a regression in Django 2.2 where deprecation message crashes if
   ``Meta.ordering`` contains an expression (:ticket:`30463`).
+
+* Fixed a regression in Django 2.2.1 where
+  :class:`~django.contrib.postgres.search.SearchVector` generates SQL with a
+  redundant ``Coalesce`` call (:ticket:`30488`).
diff --git a/tests/postgres_tests/test_search.py b/tests/postgres_tests/test_search.py
index 8944c6342d..f5111ce8d3 100644
--- a/tests/postgres_tests/test_search.py
+++ b/tests/postgres_tests/test_search.py
@@ -113,6 +113,10 @@ class SearchVectorFieldTest(GrailTestData, PostgreSQLTestCase):
         searched = Line.objects.filter(dialogue_search_vector=SearchQuery('cadeaux', config='french'))
         self.assertSequenceEqual(searched, [self.french])
 
+    def test_single_coalesce_expression(self):
+        searched = Line.objects.annotate(search=SearchVector('dialogue')).filter(search='cadeaux')
+        self.assertNotIn('COALESCE(COALESCE', str(searched.query))
+
 
 class MultipleFieldsTest(GrailTestData, PostgreSQLTestCase):