diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj
index 2749cf4cbbacba6efa2da4145eb7ea87f5840c02..02afbb27c3444ef1cd0e9c4ee1100711a02329ee 100644
--- a/src/metabase/query_processor.clj
+++ b/src/metabase/query_processor.clj
@@ -307,6 +307,10 @@
               (preprocess* query)))]
     (qp query nil nil)))
 
+(defn- restore-join-aliases [preprocessed-query]
+  (let [replacement (-> preprocessed-query :info :alias/escaped->original)]
+    (escape-join-aliases/restore-aliases preprocessed-query replacement)))
+
 (defn query->expected-cols
   "Return the `:cols` you would normally see in MBQL query results by preprocessing the query and calling `annotate` on
   it. This only works for pure MBQL queries, since it does not actually run the queries. Native queries or MBQL
@@ -314,11 +318,11 @@
   [{query-type :type, :as query}]
   (when-not (= (mbql.u/normalize-token query-type) :query)
     (throw (ex-info (tru "Can only determine expected columns for MBQL queries.")
-             {:type qp.error-type/qp})))
+                    {:type qp.error-type/qp})))
   ;; TODO - we should throw an Exception if the query has a native source query or at least warn about it. Need to
   ;; check where this is used.
   (qp.store/with-store
-    (let [preprocessed (preprocess query)]
+    (let [preprocessed (-> query preprocess restore-join-aliases)]
       (driver/with-driver (driver.u/database->driver (:database preprocessed))
         (not-empty (vec (annotate/merged-column-info preprocessed nil)))))))
 
diff --git a/test/metabase/query_processor/util/add_alias_info_test.clj b/test/metabase/query_processor/util/add_alias_info_test.clj
index a37eaf40cd0edc93046642ef2d42d2472366c6e7..282384220045b557bd98e769799acd82f4eda45d 100644
--- a/test/metabase/query_processor/util/add_alias_info_test.clj
+++ b/test/metabase/query_processor/util/add_alias_info_test.clj
@@ -570,6 +570,45 @@
                             add-alias-info
                             :query)))))))))
 
+(deftest query->expected-cols-test
+  (testing "field_refs in expected columns have the original join aliases (#30648)"
+    (mt/dataset sample-dataset
+      (binding [driver/*driver* ::custom-escape-spaces-to-underscores]
+        (let [query
+              (mt/mbql-query
+               products
+                {:joins
+                 [{:source-query
+                   {:source-table $$orders
+                    :joins
+                    [{:source-table $$people
+                      :alias "People"
+                      :condition [:= $orders.user_id &People.people.id]
+                      :fields [&People.people.address]
+                      :strategy :left-join}]
+                    :fields [$orders.id &People.people.address]}
+                   :alias "Question 54"
+                   :condition [:= $id [:field %orders.id {:join-alias "Question 54"}]]
+                   :fields [[:field %orders.id {:join-alias "Question 54"}]
+                            [:field %people.address {:join-alias "Question 54"}]]
+                   :strategy :left-join}]
+                 :fields
+                 [!default.created_at
+                  [:field %orders.id {:join-alias "Question 54"}]
+                  [:field %people.address {:join-alias "Question 54"}]]})]
+          (is (=? [{:name "CREATED_AT"
+                    :field_ref [:field (mt/id :products :created_at) {:temporal-unit :default}]
+                    :display_name "Created At"}
+                   {:name "ID"
+                    :field_ref [:field (mt/id :orders :id) {:join-alias "Question 54"}]
+                    :display_name "Question 54 → ID"
+                    :source_alias "Question 54"}
+                   {:name "ADDRESS"
+                    :field_ref [:field (mt/id :people :address) {:join-alias "Question 54"}]
+                    :display_name "Question 54 → Address"
+                    :source_alias "Question 54"}]
+                  (qp/query->expected-cols query))))))))
+
 (deftest use-source-unique-aliases-test
   (testing "Make sure uniquified aliases in the source query end up getting used for `::add/source-alias`"
     ;; keep track of the IDs so we don't accidentally fetch the wrong ones after we switch the name of `price`