internal/postgres: optimize getSymbolHistoryForBuildContext query

getSymbolHistoryForBuildContext now accepts a pathID instead of
packagePath as an argument, to avoid a JOIN on the paths table.

For golang/go#37102

Change-Id: I4e6ac004eb56f2c7744c2ebd7b0e8dcd995f0f4f
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/316230
Trust: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/postgres/path.go b/internal/postgres/path.go
index 239a0be..82c0457 100644
--- a/internal/postgres/path.go
+++ b/internal/postgres/path.go
@@ -167,3 +167,10 @@
 	}
 	return pathToID, nil
 }
+
+func getPathID(ctx context.Context, ddb *database.DB, path string) (id int, err error) {
+	err = ddb.QueryRow(ctx,
+		`SELECT id FROM paths WHERE path = $1`,
+		path).Scan(&id)
+	return id, err
+}
diff --git a/internal/postgres/symbol_history.go b/internal/postgres/symbol_history.go
index 9c8272a..50c03c5 100644
--- a/internal/postgres/symbol_history.go
+++ b/internal/postgres/symbol_history.go
@@ -109,6 +109,8 @@
 // GetSymbolHistoryWithPackageSymbols is exported for use in tests.
 func GetSymbolHistoryWithPackageSymbols(ctx context.Context, ddb *database.DB,
 	packagePath, modulePath string) (_ map[string]map[string]*internal.UnitSymbol, err error) {
+	defer derrors.WrapStack(&err, "GetSymbolHistoryWithPackageSymbols(ctx, ddb, %q, %q)", packagePath, modulePath)
+	defer middleware.ElapsedStat(ctx, "GetSymbolHistoryWithPackageSymbols")()
 	versionToNameToUnitSymbols, err := getPackageSymbols(ctx, ddb, packagePath, modulePath)
 	if err != nil {
 		return nil, err
@@ -116,39 +118,18 @@
 	return symbol.IntroducedHistory(versionToNameToUnitSymbols), nil
 }
 
-// GetSymbolHistoryForBuildContext returns a map of the first version when a symbol name is
+// getSymbolHistoryForBuildContext returns a map of the first version when a symbol name is
 // added to the API for the specified build context, to the symbol name, to the
 // UnitSymbol struct. The UnitSymbol.Children field will always be empty, as
 // children names are also tracked.
-func (db *DB) GetSymbolHistoryForBuildContext(ctx context.Context, packagePath, modulePath string,
-	build internal.BuildContext) (nameToVersion map[string]string, err error) {
-	defer derrors.Wrap(&err, "GetSymbolHistoryForBuildContext(ctx, %q, %q)", packagePath, modulePath)
-	defer middleware.ElapsedStat(ctx, "GetSymbolHistoryForBuildContext")()
-
-	if experiment.IsActive(ctx, internal.ExperimentReadSymbolHistory) {
-		if build.GOOS == internal.All {
-			// It doesn't matter which one we use, so just pick a random one.
-			build = internal.BuildContextLinux
-		}
-		return getSymbolHistoryForBuildContext(ctx, db.db, packagePath, modulePath, build)
-	}
-
-	versionToNameToUnitSymbol, err := GetSymbolHistoryWithPackageSymbols(ctx, db.db, packagePath, modulePath)
-	if err != nil {
-		return nil, err
-	}
-	nameToVersion = map[string]string{}
-	for v, nts := range versionToNameToUnitSymbol {
-		for n := range nts {
-			nameToVersion[n] = v
-		}
-	}
-	return nameToVersion, nil
-}
-
-func getSymbolHistoryForBuildContext(ctx context.Context, ddb *database.DB, packagePath, modulePath string,
+func getSymbolHistoryForBuildContext(ctx context.Context, ddb *database.DB, pathID int, modulePath string,
 	bc internal.BuildContext) (_ map[string]string, err error) {
-	defer derrors.WrapStack(&err, "getSymbolHistoryForBuildContext(ctx, ddb, %q, %q)", packagePath, modulePath)
+	defer derrors.WrapStack(&err, "getSymbolHistoryForBuildContext(ctx, ddb, %d, %q)", pathID, modulePath)
+	defer middleware.ElapsedStat(ctx, "getSymbolHistoryForBuildContext")()
+
+	if bc == internal.BuildContextAll {
+		bc = internal.BuildContextLinux
+	}
 
 	q := squirrel.Select(
 		"s1.name AS symbol_name",
@@ -157,9 +138,8 @@
 		Join("package_symbols ps ON ps.id = sh.package_symbol_id").
 		Join("symbol_names s1 ON ps.symbol_name_id = s1.id").
 		Join("symbol_names s2 ON ps.parent_symbol_name_id = s2.id").
-		Join("paths p1 ON sh.package_path_id = p1.id").
 		Join("paths p2 ON sh.module_path_id = p2.id").
-		Where(squirrel.Eq{"p1.path": packagePath}).
+		Where(squirrel.Eq{"sh.package_path_id": pathID}).
 		Where(squirrel.Eq{"p2.path": modulePath}).
 		Where(squirrel.Eq{"sh.goos": bc.GOOS}).
 		Where(squirrel.Eq{"sh.goarch": bc.GOARCH})
diff --git a/internal/postgres/symbol_test.go b/internal/postgres/symbol_test.go
index 8bc7218..04d42a6 100644
--- a/internal/postgres/symbol_test.go
+++ b/internal/postgres/symbol_test.go
@@ -404,8 +404,12 @@
 		t.Fatalf("mismatch on symbol history(-want +got):\n%s", diff)
 	}
 
-	gotHist2, err := testDB.GetSymbolHistoryForBuildContext(ctx,
-		mod10.Packages()[0].Path, mod10.ModulePath, internal.BuildContextWindows)
+	pathID, err := getPathID(ctx, testDB.db, mod10.Packages()[0].Path)
+	if err != nil {
+		t.Fatal(err)
+	}
+	gotHist2, err := getSymbolHistoryForBuildContext(ctx, testDB.db,
+		pathID, mod10.ModulePath, internal.BuildContextWindows)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/postgres/unit.go b/internal/postgres/unit.go
index d19f72a..312f80f 100644
--- a/internal/postgres/unit.go
+++ b/internal/postgres/unit.go
@@ -403,10 +403,10 @@
 	defer middleware.ElapsedStat(ctx, "getUnitWithAllFields")()
 
 	// Get build contexts and unit ID.
-	var unitID int
+	var pathID, unitID int
 	var bcs []internal.BuildContext
 	err = db.db.RunQuery(ctx, `
-		SELECT d.goos, d.goarch, u.id
+		SELECT d.goos, d.goarch, u.id, p.id
 		FROM units u
 		INNER JOIN paths p ON p.id = u.path_id
 		INNER JOIN modules m ON m.id = u.module_id
@@ -419,7 +419,7 @@
 		var bc internal.BuildContext
 		// GOOS and GOARCH will be NULL if there are no documentation rows for
 		// the unit, but we still want the unit ID.
-		if err := rows.Scan(database.NullIsEmpty(&bc.GOOS), database.NullIsEmpty(&bc.GOARCH), &unitID); err != nil {
+		if err := rows.Scan(database.NullIsEmpty(&bc.GOOS), database.NullIsEmpty(&bc.GOARCH), &unitID, &pathID); err != nil {
 			return err
 		}
 		if bc.GOOS != "" && bc.GOARCH != "" {
@@ -512,12 +512,29 @@
 	u.Subdirectories = pkgs
 	u.UnitMeta = *um
 
-	if experiment.IsActive(ctx, internal.ExperimentSymbolHistoryMainPage) {
-		u.SymbolHistory, err = db.GetSymbolHistoryForBuildContext(ctx, um.Path, um.ModulePath, bcMatched)
+	if !experiment.IsActive(ctx, internal.ExperimentSymbolHistoryMainPage) {
+		return &u, nil
+	}
+	if experiment.IsActive(ctx, internal.ExperimentReadSymbolHistory) {
+		u.SymbolHistory, err = getSymbolHistoryForBuildContext(ctx, db.db, pathID, um.ModulePath, bcMatched)
 		if err != nil {
 			return nil, err
 		}
+		return &u, nil
 	}
+
+	versionToNameToUnitSymbol, err := GetSymbolHistoryWithPackageSymbols(ctx, db.db, um.Path,
+		um.ModulePath)
+	if err != nil {
+		return nil, err
+	}
+	nameToVersion := map[string]string{}
+	for v, nts := range versionToNameToUnitSymbol {
+		for n := range nts {
+			nameToVersion[n] = v
+		}
+	}
+	u.SymbolHistory = nameToVersion
 	return &u, nil
 }