diff --git a/pkg/sql/catalog/internal/validate/validate.go b/pkg/sql/catalog/internal/validate/validate.go
index 616dd23ed605..3d1d834bccbe 100644
--- a/pkg/sql/catalog/internal/validate/validate.go
+++ b/pkg/sql/catalog/internal/validate/validate.go
@@ -58,6 +58,15 @@ func Validate(
 	targetLevel catalog.ValidationLevel,
 	descriptors ...catalog.Descriptor,
 ) catalog.ValidationErrors {
+	for i, d := range descriptors {
+		// Replace mutable descriptors with immutable copies. Validation is
+		// read-only in any case, and using immutables can have a significant
+		// impact on performance when validating tables due to columns, indexes,
+		// and so forth being cached.
+		if mut, ok := d.(catalog.MutableDescriptor); ok {
+			descriptors[i] = mut.ImmutableCopy()
+		}
+	}
 	vea := validationErrorAccumulator{
 		ValidationTelemetry: telemetry,
 		targetLevel:         targetLevel,
@@ -409,9 +418,17 @@ func (cs *collectorState) getMissingDescs(
 		return nil, err
 	}
 	for _, desc := range resps {
-		if desc != nil {
-			cs.vdg.descriptors[desc.GetID()] = desc
+		if desc == nil {
+			continue
+		}
+		if mut, ok := desc.(catalog.MutableDescriptor); ok {
+			// Replace mutable descriptors with immutable copies. Validation is
+			// read-only in any case, and using immutables can have a significant
+			// impact on performance when validating tables due to columns, indexes,
+			// and so forth being cached.
+			desc = mut.ImmutableCopy()
 		}
+		cs.vdg.descriptors[desc.GetID()] = desc
 	}
 	return resps, nil
 }