diff --git a/src/metabase/query_processor/pivot.clj b/src/metabase/query_processor/pivot.clj
index ecbca94d08165e47b158348344797add0bde2711..86c736a1968b097482e5a6c39200355c9bd42542 100644
--- a/src/metabase/query_processor/pivot.clj
+++ b/src/metabase/query_processor/pivot.clj
@@ -45,8 +45,15 @@
 (mr/def ::num-breakouts ::lib.schema.common/int-greater-than-or-equal-to-zero)
 (mr/def ::index         ::lib.schema.common/int-greater-than-or-equal-to-zero)
 
-(mr/def ::pivot-rows [:sequential ::index])
-(mr/def ::pivot-cols [:sequential ::index])
+(mr/def ::pivot-rows     [:sequential ::index])
+(mr/def ::pivot-cols     [:sequential ::index])
+(mr/def ::pivot-measures [:sequential ::index])
+
+(mr/def ::pivot-opts [:maybe
+                      [:map
+                       [:pivot-rows     {:optional true} [:maybe ::pivot-rows]]
+                       [:pivot-cols     {:optional true} [:maybe ::pivot-cols]]
+                       [:pivot-measures {:optional true} [:maybe ::pivot-measures]]]])
 
 (mu/defn- group-bitmask :- ::bitmask
   "Come up with a display name given a combination of breakout `indexes` e.g.
@@ -173,9 +180,7 @@
 (mu/defn- generate-queries :- [:sequential ::lib.schema/query]
   "Generate the additional queries to perform a generic pivot table"
   [query                                               :- ::lib.schema/query
-   {:keys [pivot-rows pivot-cols], :as _pivot-options} :- [:map
-                                                           [:pivot-rows {:optional true} [:maybe ::pivot-rows]]
-                                                           [:pivot-cols {:optional true} [:maybe ::pivot-cols]]]]
+   {:keys [pivot-rows pivot-cols], :as _pivot-options} :- ::pivot-opts]
   (try
     (let [all-breakouts (lib/breakouts query)
           all-queries   (for [breakout-indexes (u/prog1 (breakout-combinations (count all-breakouts) pivot-rows pivot-cols)
@@ -342,48 +347,45 @@
               qp.pipeline/*reduce*  (or reduce qp.pipeline/*reduce*)]
       (qp/process-query first-query rff))))
 
-(mu/defn- pivot-options :- [:map
-                            [:pivot-rows [:maybe [:sequential [:int {:min 0}]]]]
-                            [:pivot-cols [:maybe [:sequential [:int {:min 0}]]]]]
+(mu/defn- pivot-options :- ::pivot-opts
   "Given a pivot table query and a card ID, looks at the `pivot_table.column_split` key in the card's visualization
   settings and generates pivot-rows and pivot-cols to use for generating subqueries."
   [query        :- [:map
                     [:database ::lib.schema.id/database]]
    viz-settings :- [:maybe :map]]
-  (let [column-split         (:pivot_table.column_split viz-settings)
-        column-split-rows    (seq (:rows column-split))
-        column-split-columns (seq (:columns column-split))
-        index-in-breakouts   (when (or column-split-rows
-                                       column-split-columns)
-                               (let [metadata-provider (or (:lib/metadata query)
-                                                           (lib.metadata.jvm/application-database-metadata-provider (:database query)))
-                                     mlv2-query        (lib/query metadata-provider query)
-                                     breakouts         (into []
-                                                             (map-indexed (fn [i col]
-                                                                            (cond-> col
-                                                                              true                         (assoc ::i i)
-                                                                              ;; if the col has a card-id, we swap the :lib/source to say source/card
-                                                                              ;; this allows `lib/find-matching-column` to properly match a column that has a join-alias
-                                                                              ;; but whose source is a model
-                                                                              (contains? col :lib/card-id) (assoc :lib/source :source/card))))
-                                                             (lib/breakouts-metadata mlv2-query))]
-                                 (fn [legacy-ref]
-                                   (try
-                                     (::i (lib.equality/find-column-for-legacy-ref
-                                           mlv2-query
-                                           -1
-                                           legacy-ref
-                                           breakouts))
-                                     (catch Throwable e
-                                       (log/errorf e "Error finding matching column for ref %s" (pr-str legacy-ref))
-                                       nil)))))
-
-        pivot-rows (when column-split-rows
-                     (into [] (keep index-in-breakouts) column-split-rows))
-        pivot-cols (when column-split-columns
-                     (into [] (keep index-in-breakouts) column-split-columns))]
-    {:pivot-rows pivot-rows
-     :pivot-cols pivot-cols}))
+  (let [{:keys [rows columns values]} (:pivot_table.column_split viz-settings)
+        metadata-provider             (or (:lib/metadata query)
+                                          (lib.metadata.jvm/application-database-metadata-provider (:database query)))
+        mlv2-query                    (lib/query metadata-provider query)
+        breakouts                     (into []
+                                            (map-indexed (fn [i col]
+                                                           (cond-> col
+                                                             true                         (assoc ::idx i)
+                                                             ;; if the col has a card-id, we swap the :lib/source to say
+                                                             ;; source/card this allows `lib/find-matching-column` to properly
+                                                             ;; match a column that has a join-alias but whose source is a
+                                                             ;; model
+                                                             (contains? col :lib/card-id) (assoc :lib/source :source/card))))
+                                            (concat (lib/breakouts-metadata mlv2-query)
+                                                    (lib/aggregations-metadata mlv2-query)))
+        index-in-breakouts            (fn index-in-breakouts [legacy-ref]
+                                        (try
+                                          (::idx (lib.equality/find-column-for-legacy-ref
+                                                  mlv2-query
+                                                  -1
+                                                  legacy-ref
+                                                  breakouts))
+                                          (catch Throwable e
+                                            (log/errorf e "Error finding matching column for ref %s" (pr-str legacy-ref))
+                                            nil)))
+        process-refs                  (fn process-refs [refs]
+                                        (when (seq refs)
+                                          (into [] (keep index-in-breakouts) refs)))
+        pivot-opts                    {:pivot-rows     (process-refs rows)
+                                       :pivot-cols     (process-refs columns)
+                                       :pivot-measures (process-refs values)}]
+    (when (some some? (vals pivot-opts))
+      pivot-opts)))
 
 (mu/defn- column-mapping-for-subquery :- ::pivot-column-mapping
   [num-canonical-cols            :- ::lib.schema.common/int-greater-than-or-equal-to-zero
@@ -529,10 +531,11 @@
      (qp.setup/with-qp-setup [query query]
        (let [rff               (or rff qp.reducible/default-rff)
              query             (lib/query (qp.store/metadata-provider) query)
-             pivot-options     (or
-                                (not-empty (select-keys query [:pivot-rows :pivot-cols]))
-                                (pivot-options query (get-in query [:info :visualization-settings])))
-             query             (assoc-in query [:middleware :pivot-options] pivot-options)
-             all-queries       (generate-queries query pivot-options)
+             pivot-opts        (or
+                                (pivot-options query (get query :viz-settings))
+                                (pivot-options query (get-in query [:info :visualization-settings]))
+                                (not-empty (select-keys query [:pivot-rows :pivot-cols :pivot-measures])))
+             query             (assoc-in query [:middleware :pivot-options] pivot-opts)
+             all-queries       (generate-queries query pivot-opts)
              column-mapping-fn (make-column-mapping-fn query)]
          (process-multiple-queries all-queries rff column-mapping-fn))))))
diff --git a/src/metabase/query_processor/pivot/postprocess.clj b/src/metabase/query_processor/pivot/postprocess.clj
index 7b277a2700147dfd05ee577a4d80133c059c2c13..a70414848ed9fe041381cc4b3efacefd56743a56 100644
--- a/src/metabase/query_processor/pivot/postprocess.clj
+++ b/src/metabase/query_processor/pivot/postprocess.clj
@@ -62,10 +62,19 @@
 
 (mu/defn add-pivot-measures :- ::pivot-spec
   "Given a pivot-spec map without the `:pivot-measures` key, determine what key(s) the measures will be and assoc that value into `:pivot-measures`."
-  [pivot-spec :- ::pivot-spec]
-  (-> pivot-spec
-      (assoc :pivot-measures (pivot-measures pivot-spec))
-      (assoc :pivot-grouping (pivot-grouping-key (:column-titles pivot-spec)))))
+  [{measure-indices :pivot-measures :as pivot-spec} :- ::pivot-spec]
+  (let [pivot-grouping-key (pivot-grouping-key (:column-titles pivot-spec))]
+    (cond-> pivot-spec
+      ;; if pivot-measures don't already exist (from the pivot qp), we add them ourselves, assuming lowest ID -> highest ID sort order
+      (not (seq measure-indices)) (assoc :pivot-measures (pivot-measures pivot-spec))
+      ;; otherwise, we modify indices to skip over whatever the pivot-grouping idx is, so we pull the correct values per row
+      (seq measure-indices)       (update :pivot-measures (fn [indices]
+                                                            (mapv (fn [idx]
+                                                                    (if (>= idx pivot-grouping-key)
+                                                                      (inc idx)
+                                                                      idx))
+                                                                  indices)))
+      true                       (assoc :pivot-grouping pivot-grouping-key))))
 
 (mu/defn add-totals-settings :- ::pivot-spec
   "Given a pivot-spec map and `viz-settings`, add the `:row-totals?` and `:col-totals?` keys."
diff --git a/src/metabase/query_processor/streaming/csv.clj b/src/metabase/query_processor/streaming/csv.clj
index 99b2d0173e598b79779b91115482799c3cf34e35..ab1a6529d3d133928a66352d3688e92ff52bf9db 100644
--- a/src/metabase/query_processor/streaming/csv.clj
+++ b/src/metabase/query_processor/streaming/csv.clj
@@ -78,15 +78,14 @@
       (begin! [_ {{:keys [ordered-cols results_timezone format-rows? pivot-export-options pivot?]
                    :or   {format-rows? true
                           pivot?       false}} :data} viz-settings]
-        (let [opts               (when (and pivot? pivot-export-options)
+        (let [col-names          (vec (common/column-titles ordered-cols (::mb.viz/column-settings viz-settings) format-rows?))
+              opts               (when (and pivot? pivot-export-options)
                                    (-> (merge {:pivot-rows []
                                                :pivot-cols []}
                                               pivot-export-options)
-                                       (assoc :column-titles (mapv :display_name ordered-cols))
+                                       (assoc :column-titles col-names)
                                        (qp.pivot.postprocess/add-totals-settings viz-settings)
                                        qp.pivot.postprocess/add-pivot-measures))
-              ;; col-names are created later when exporting a pivot table, so only create them if there are no pivot options
-              col-names          (when-not opts (common/column-titles ordered-cols (::mb.viz/column-settings viz-settings) format-rows?))
               pivot-grouping-key (qp.pivot.postprocess/pivot-grouping-key col-names)]
 
           ;; initialize the pivot-data
@@ -101,7 +100,7 @@
                    (mapv #(formatter/create-formatter results_timezone % viz-settings format-rows?) ordered-cols))
 
           ;; write the column names for non-pivot tables
-          (when col-names
+          (when (not opts)
             (let [header (m/remove-nth (or pivot-grouping-key (inc (count col-names))) col-names)]
               (write-csv writer [header])
               (.flush writer)))))
diff --git a/src/metabase/query_processor/streaming/json.clj b/src/metabase/query_processor/streaming/json.clj
index 2f851442d98bd16b36bb520a8d94796c6abe1a7f..05cf3de2c42422b0db0dd77dd377c846bfe4dace 100644
--- a/src/metabase/query_processor/streaming/json.clj
+++ b/src/metabase/query_processor/streaming/json.clj
@@ -41,8 +41,6 @@
     (reify qp.si/StreamingResultsWriter
       (begin! [_ {{:keys [ordered-cols results_timezone format-rows?]
                    :or   {format-rows? true}} :data} viz-settings]
-        ;; TODO -- wouldn't it make more sense if the JSON downloads used `:name` preferentially? Seeing how JSON is
-        ;; probably going to be parsed programmatically
         (let [cols           (common/column-titles ordered-cols (::mb.viz/column-settings viz-settings) format-rows?)
               pivot-grouping (qp.pivot.postprocess/pivot-grouping-key cols)]
           (when pivot-grouping (vreset! pivot-grouping-idx pivot-grouping))
diff --git a/test/metabase/api/downloads_exports_test.clj b/test/metabase/api/downloads_exports_test.clj
index fb7299965a8b15d434d7ec61394341ede4bc3fea..283f34bac7d380f3bba9d5394e1e35fa47afc984 100644
--- a/test/metabase/api/downloads_exports_test.clj
+++ b/test/metabase/api/downloads_exports_test.clj
@@ -1061,32 +1061,108 @@
                                                        :type     :native
                                                        :native   {:query "SELECT 1234.567 as A"}}}]
         (testing "CSV downloads respect the formatted/unformatted setting"
-          (let [formatted-json-results   (all-downloads card {:export-format :csv :format-rows true})
-                unformatted-json-results (all-downloads card {:export-format :csv :format-rows false})]
+          (let [formatted-results   (all-downloads card {:export-format :csv :format-rows true})
+                unformatted-results (all-downloads card {:export-format :csv :format-rows false})]
             (is (= {:unsaved-card-download    [["A"] ["1,234.57"]]
                     :card-download            [["A"] ["1,234.57"]]
                     :public-question-download [["A"] ["1,234.57"]]
                     :dashcard-download        [["A"] ["1,234.57"]]
                     :public-dashcard-download [["A"] ["1,234.57"]]}
-                   formatted-json-results))
+                   formatted-results))
             (is (= {:unsaved-card-download    [["A"] ["1234.567"]]
                     :card-download            [["A"] ["1234.567"]]
                     :public-question-download [["A"] ["1234.567"]]
                     :dashcard-download        [["A"] ["1234.567"]]
                     :public-dashcard-download [["A"] ["1234.567"]]}
-                   unformatted-json-results))))
+                   unformatted-results))))
         (testing "JSON downloads respect the formatted/unformatted setting"
-          (let [formatted-json-results   (all-downloads card {:export-format :json :format-rows true})
-                unformatted-json-results (all-downloads card {:export-format :json :format-rows false})]
+          (let [formatted-results   (all-downloads card {:export-format :json :format-rows true})
+                unformatted-results (all-downloads card {:export-format :json :format-rows false})]
             (is (= {:unsaved-card-download    [["A"] ["1,234.57"]]
                     :card-download            [["A"] ["1,234.57"]]
                     :public-question-download [["A"] ["1,234.57"]]
                     :dashcard-download        [["A"] ["1,234.57"]]
                     :public-dashcard-download [["A"] ["1,234.57"]]}
-                   formatted-json-results))
+                   formatted-results))
             (is (= {:unsaved-card-download    [["A"] [1234.567]]
                     :card-download            [["A"] [1234.567]]
                     :public-question-download [["A"] [1234.567]]
                     :dashcard-download        [["A"] [1234.567]]
                     :public-dashcard-download [["A"] [1234.567]]}
-                   unformatted-json-results))))))))
+                   unformatted-results))))))))
+
+(deftest pivot-measures-order-test
+  (testing "A pivot download will use the user-configured measures order (#48442)."
+    (mt/dataset test-data
+      (mt/with-temp [:model/Card card {:display                :pivot
+                                       :dataset_query          {:database (mt/id)
+                                                                :type     :query
+                                                                :query
+                                                                {:source-table (mt/id :products)
+                                                                 :aggregation  [[:count]
+                                                                                [:sum [:field (mt/id :products :price) {:base-type :type/Float}]]
+                                                                                [:avg [:field (mt/id :products :rating) {:base-type :type/Float}]]]
+                                                                 :breakout     [[:field (mt/id :products :category) {:base-type :type/Text}]]}}
+                                       :visualization_settings {:pivot_table.column_split
+                                                                {:rows    [[:field (mt/id :products :category) {:base-type :type/Text}]]
+                                                                 :columns []
+                                                                 :values  [[:aggregation 1]
+                                                                           [:aggregation 0]
+                                                                           [:aggregation 2]]}
+                                                                :column_settings
+                                                                {"[\"name\",\"count\"]" {:column_title "Count Renamed"}
+                                                                 "[\"name\",\"sum\"]"   {:column_title "Sum Renamed"}
+                                                                 "[\"name\",\"avg\"]"   {:column_title "Average Renamed"}}}}]
+        (let [expected-header   ["Category" "Sum of Price" "Count" "Average of Rating"]
+              formatted-results (all-downloads card {:export-format :csv :format-rows false :pivot true})]
+          (is (= {:unsaved-card-download    expected-header
+                  :card-download            expected-header
+                  :public-question-download expected-header
+                  :dashcard-download        expected-header
+                  :public-dashcard-download expected-header}
+                 (update-vals formatted-results first))))
+        (testing "The column title changes are used when format-rows is true"
+          (let [expected-header   ["Category" "Sum Renamed" "Count Renamed" "Average Renamed"]
+                formatted-results (all-downloads card {:export-format :csv :format-rows true :pivot true})]
+            (is (= {:unsaved-card-download    expected-header
+                    :card-download            expected-header
+                    :public-question-download expected-header
+                    :dashcard-download        expected-header
+                    :public-dashcard-download expected-header}
+                   (update-vals formatted-results first)))))))))
+
+(deftest pivot-rows-order-test
+  (testing "A pivot download will use the user-configured rows order."
+    (mt/dataset test-data
+      (mt/with-temp [:model/Card card {:display                :pivot
+                                       :dataset_query          {:database (mt/id)
+                                                                :type     :query
+                                                                :query
+                                                                {:source-table (mt/id :products)
+                                                                 :aggregation  [[:count]]
+                                                                 :breakout     [[:field (mt/id :products :category) {:base-type :type/Text}]
+                                                                                [:field (mt/id :products :created_at) {:base-type :type/DateTime :temporal-unit :year}]]}}
+                                       :visualization_settings {:pivot_table.column_split
+                                                                {:rows    [[:field (mt/id :products :created_at) {:base-type :type/DateTime :temporal-unit :year}]
+                                                                           [:field (mt/id :products :category) {:base-type :type/Text}]]
+                                                                 :columns []
+                                                                 :values  [[:aggregation 0]]}
+                                                                :column_settings
+                                                                {"[\"name\",\"count\"]" {:column_title "Count Renamed"}}}}]
+        (let [expected-header   ["Created At" "Category" "Count"]
+              formatted-results (all-downloads card {:export-format :csv :format-rows false :pivot true})]
+          (is (= {:unsaved-card-download    expected-header
+                  :card-download            expected-header
+                  :public-question-download expected-header
+                  :dashcard-download        expected-header
+                  :public-dashcard-download expected-header}
+                 (update-vals formatted-results first))))
+        (testing "The column title changes are used when format-rows is true"
+          (let [expected-header   ["Created At" "Category" "Count Renamed"]
+                formatted-results (all-downloads card {:export-format :csv :format-rows true :pivot true})]
+            (is (= {:unsaved-card-download    expected-header
+                    :card-download            expected-header
+                    :public-question-download expected-header
+                    :dashcard-download        expected-header
+                    :public-dashcard-download expected-header}
+                   (update-vals formatted-results first)))))))))
diff --git a/test/metabase/query_processor/pivot_test.clj b/test/metabase/query_processor/pivot_test.clj
index ca255bae82cd94174abd26a2a4a3796531325cb6..e2bf1cb002457cddd5fa1d93acbdb616b254ce3e 100644
--- a/test/metabase/query_processor/pivot_test.clj
+++ b/test/metabase/query_processor/pivot_test.clj
@@ -191,7 +191,7 @@
   (testing "`pivot-options` correctly generates pivot-rows and pivot-cols from a card's viz settings"
     (let [query         (api.pivots/pivot-query false)
           viz-settings  (:visualization_settings (api.pivots/pivot-card))
-          pivot-options {:pivot-rows [1 0], :pivot-cols [2]}]
+          pivot-options {:pivot-rows [1 0], :pivot-cols [2] :pivot-measures nil}]
       (is (= pivot-options
              (#'qp.pivot/pivot-options query viz-settings)))
       (are [num-breakouts expected] (= expected
@@ -217,7 +217,7 @@
                              :columns [[:field "RATING" {:base-type :type/Integer}]]}})
                          [:pivot_table.column_split])
           pivot-options (#'qp.pivot/pivot-options query viz-settings)]
-      (is (= {:pivot-rows [], :pivot-cols []}
+      (is (= {:pivot-rows [], :pivot-cols [] :pivot-measures nil}
              pivot-options))
       (is (= [[0 1] [1] [0] []]
              (#'qp.pivot/breakout-combinations 2 (:pivot-rows pivot-options) (:pivot-cols pivot-options)))))))
@@ -245,7 +245,7 @@
                                  {:rows    [$category]
                                   :columns [$created_at]}})
                 pivot-options (#'qp.pivot/pivot-options query viz-settings)]
-            (is (= {:pivot-rows [0], :pivot-cols [1]}
+            (is (= {:pivot-rows [0], :pivot-cols [1] :pivot-measures nil}
                    pivot-options))
             (is (= [[0 1] [1] [0] []]
                    (#'qp.pivot/breakout-combinations 2 (:pivot-rows pivot-options) (:pivot-cols pivot-options))))