[gold] Change CommitID to be a string.

This helps us support Chrome OS (and could decouple
us from git). This also allows us to assign variable tile
widths across instances and time if we want by adding
TileID to the Commits table.

While I was introducing this, I changed diffcalculator and
diff/worker to use a number of dense commits instead of a
number of tiles. Otherwise, I would have to do that first
in a completely different way and then redo it here.

Suggested review order:
 1) schema/tables.go
 2) databuilder.go
 3) kitchensink.go
 4) worker_test.go
 5) worker.go
 6) everything else.

Bug: skia:11367
Change-Id: Iaa4737bca1e1e153e4c7d5fa684ee65cc2f51590
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/375659
Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
diff --git a/golden/cmd/diffcalculator/diffcalculator.go b/golden/cmd/diffcalculator/diffcalculator.go
index d47ebaa..82a432d 100644
--- a/golden/cmd/diffcalculator/diffcalculator.go
+++ b/golden/cmd/diffcalculator/diffcalculator.go
@@ -46,6 +46,10 @@
 type diffCalculatorConfig struct {
 	config.Common
 
+	// CommitsWithDataToSearch is how many commits we should go back in time through to find
+	// images to diff against.
+	CommitsWithDataToSearch int `json:"commits_with_data_to_search"`
+
 	// DiffCacheNamespace is a namespace for differentiating the DiffCache entities. The instance
 	// name is fine here.
 	DiffCacheNamespace string `json:"diff_cache_namespace" optional:"true"`
@@ -71,10 +75,6 @@
 
 	// The port to provide a web handler for /healthz
 	ReadyPort string `json:"ready_port"`
-
-	// TileToProcess is how many tiles of commits we should use as the number of available digests
-	// to diff.
-	TilesToProcess int `json:"tiles_to_process"`
 }
 
 func main() {
@@ -118,7 +118,7 @@
 	gis := mustMakeGCSImageSource(ctx, dcc)
 	diffcache := mustMakeDiffCache(ctx, dcc)
 	sqlProcessor := &processor{
-		calculator:  worker.New(db, gis, diffcache, dcc.TilesToProcess),
+		calculator:  worker.New(db, gis, diffcache, dcc.CommitsWithDataToSearch),
 		ackCounter:  metrics2.GetCounter("diffcalculator_ack"),
 		nackCounter: metrics2.GetCounter("diffcalculator_nack"),
 	}
diff --git a/golden/go/diff/worker/worker.go b/golden/go/diff/worker/worker.go
index de58ccc..79efdd9 100644
--- a/golden/go/diff/worker/worker.go
+++ b/golden/go/diff/worker/worker.go
@@ -15,6 +15,7 @@
 
 	"github.com/dgraph-io/ristretto"
 	"github.com/jackc/pgtype"
+	"github.com/jackc/pgx/v4"
 	"github.com/jackc/pgx/v4/pgxpool"
 	ttlcache "github.com/patrickmn/go-cache"
 	"go.opencensus.io/trace"
@@ -79,22 +80,22 @@
 
 // WorkerImpl is a basic implementation that reads and writes to the SQL backend.
 type WorkerImpl struct {
-	db                *pgxpool.Pool
-	diffCache         DiffCache // should be faster to query than db for a "yes/no" answer.
-	imageSource       ImageSource
-	badDigestsCache   *ttlcache.Cache
-	decodedImageCache *ristretto.Cache
-	// TODO(kjlubick) this might not be the best parameter for "digests to compute against" as we
-	//   might just want to query for the last N commits with data and start at that tile to better
-	//   handle the sparse data case.
-	tilesToProcess           int
+	db                       *pgxpool.Pool
+	diffCache                DiffCache // should be faster to query than db for a "yes/no" answer.
+	imageSource              ImageSource
+	badDigestsCache          *ttlcache.Cache
+	decodedImageCache        *ristretto.Cache
+	commitsWithDataToSearch  int
 	metricsCalculatedCounter metrics2.Counter
 	decodedImageBytesSummary metrics2.Float64SummaryMetric
 	encodedImageBytesSummary metrics2.Float64SummaryMetric
 }
 
 // New returns a WorkerImpl that is ready to compute diffs.
-func New(db *pgxpool.Pool, src ImageSource, dc DiffCache, tilesToProcess int) *WorkerImpl {
+func New(db *pgxpool.Pool, src ImageSource, dc DiffCache, commitsWithDataToSearch int) *WorkerImpl {
+	if commitsWithDataToSearch <= 0 {
+		panic("Invalid value for commitsWithDataToSearch")
+	}
 	imgCache, err := ristretto.NewCache(&ristretto.Config{
 		NumCounters: 20_000, // we expect a few thousand decoded images to fit.
 		MaxCost:     decodedImageCacheSizeGB * 1024 * 1024 * 1024,
@@ -109,7 +110,7 @@
 		imageSource:              src,
 		badDigestsCache:          ttlcache.New(badImageCooldown, 2*badImageCooldown),
 		decodedImageCache:        imgCache,
-		tilesToProcess:           tilesToProcess,
+		commitsWithDataToSearch:  commitsWithDataToSearch,
 		metricsCalculatedCounter: metrics2.GetCounter("diffcalculator_metricscalculated"),
 		decodedImageBytesSummary: metrics2.GetFloat64SummaryMetric("diffcalculator_decodedimagebytes"),
 		encodedImageBytesSummary: metrics2.GetFloat64SummaryMetric("diffcalculator_encodedimagebytes"),
@@ -281,27 +282,29 @@
 
 // getStartingTile returns the commit ID which is the beginning of the tile of interest (so we
 // get enough data to do our comparisons).
-func (w *WorkerImpl) getStartingTile(ctx context.Context) (schema.CommitID, error) {
-	row := w.db.QueryRow(ctx, `SELECT max(commit_id) FROM Commits
+func (w *WorkerImpl) getStartingTile(ctx context.Context) (schema.TileID, error) {
+	row := w.db.QueryRow(ctx, `SELECT tile_id FROM Commits
 AS OF SYSTEM TIME '-0.1s'
-WHERE has_data = TRUE`)
+WHERE has_data = TRUE
+ORDER BY commit_id DESC
+LIMIT 1 OFFSET $1`, w.commitsWithDataToSearch-1)
 	var lc pgtype.Int4
 	if err := row.Scan(&lc); err != nil {
+		if err == pgx.ErrNoRows {
+			return 0, nil // not enough commits seen, so start at tile 0.
+		}
 		return 0, skerr.Wrapf(err, "getting latest commit")
 	}
 	if lc.Status == pgtype.Null {
-		// There are no commits with data
+		// There are no commits with data, so start at tile 0.
 		return 0, nil
 	}
-	latestCommit := schema.CommitID(lc.Int)
-	currentTileStart := sql.ComputeTileStartID(latestCommit, schema.TileWidth)
-	// Go backwards so we can use the current tile plus the previous n-1 tiles.
-	return currentTileStart - schema.CommitID(schema.TileWidth*(w.tilesToProcess-1)), nil
+	return schema.TileID(lc.Int), nil
 }
 
 // getAllExisting returns the unique digests that were seen on the primary branch for a given
 // grouping starting at the given commit.
-func (w *WorkerImpl) getAllExisting(ctx context.Context, beginTileStart schema.CommitID, grouping paramtools.Params) ([]types.Digest, error) {
+func (w *WorkerImpl) getAllExisting(ctx context.Context, beginTileStart schema.TileID, grouping paramtools.Params) ([]types.Digest, error) {
 	ctx, span := trace.StartSpan(ctx, "getAllExisting")
 	defer span.End()
 	const statement = `
@@ -311,7 +314,7 @@
 )
 SELECT DISTINCT digest FROM TiledTraceDigests
 JOIN TracesMatchingGrouping on TiledTraceDigests.trace_id = TracesMatchingGrouping.trace_id
-WHERE TiledTraceDigests.start_commit_id >= $2`
+WHERE TiledTraceDigests.tile_id >= $2`
 
 	_, groupingID := sql.SerializeMap(grouping)
 	rows, err := w.db.Query(ctx, statement, groupingID, beginTileStart)
@@ -330,7 +333,7 @@
 	return rv, nil
 }
 
-func (w *WorkerImpl) getNonIgnoredExisting(ctx context.Context, beginTileStart schema.CommitID, grouping paramtools.Params) ([]types.Digest, error) {
+func (w *WorkerImpl) getNonIgnoredExisting(ctx context.Context, beginTileStart schema.TileID, grouping paramtools.Params) ([]types.Digest, error) {
 	ctx, span := trace.StartSpan(ctx, "getNonIgnoredExisting")
 	defer span.End()
 	const statement = `
@@ -340,7 +343,7 @@
 )
 SELECT DISTINCT digest FROM TiledTraceDigests
 JOIN NotIgnoredTracesMatchingGrouping on TiledTraceDigests.trace_id = NotIgnoredTracesMatchingGrouping.trace_id
-WHERE TiledTraceDigests.start_commit_id >= $2`
+WHERE TiledTraceDigests.tile_id >= $2`
 
 	_, groupingID := sql.SerializeMap(grouping)
 	rows, err := w.db.Query(ctx, statement, groupingID, beginTileStart)
diff --git a/golden/go/diff/worker/worker_test.go b/golden/go/diff/worker/worker_test.go
index 0b7fd86..ddc6d3a 100644
--- a/golden/go/diff/worker/worker_test.go
+++ b/golden/go/diff/worker/worker_test.go
@@ -7,7 +7,6 @@
 	"fmt"
 	"io/ioutil"
 	"path/filepath"
-	"strconv"
 	"sync"
 	"testing"
 	"time"
@@ -240,11 +239,11 @@
 	fakeNow := time.Date(2021, time.February, 1, 1, 1, 1, 0, time.UTC)
 	ctx := context.WithValue(context.Background(), NowSourceKey, mockTime(fakeNow))
 	db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
-	// Only C03, C04, C05 will be in the last two tiles for this data.
+	// Only C03, C04, C05 will be in the last 3 commits for this data.
 	sparseData := makeSparseData()
 	require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, sparseData))
 	waitForSystemTime()
-	w := newWorkerUsingImagesFromKitchenSink(t, db)
+	w := New(db, &fsImageSource{root: kitchenSinkRoot(t)}, &memDiffCache{}, 3)
 
 	grouping := paramtools.Params{
 		types.CorpusField:     dks.RoundCorpus,
@@ -432,8 +431,8 @@
 	db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
 	// C04 and C05 appear only on ignored traces. C01, C02 appear only on non-ignored traces.
 	// C03 appears on both an ignored trace and an ignored trace.
-	sparseData := makeDataWithIgnoredTraces()
-	require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, sparseData))
+	data := makeDataWithIgnoredTraces()
+	require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, data))
 	waitForSystemTime()
 	w := newWorkerUsingImagesFromKitchenSink(t, db)
 
@@ -506,20 +505,14 @@
 }
 
 func makeSparseData() schema.Tables {
-	b := databuilder.TablesBuilder{}
-	// Make a few commits with data across several tiles (tile width == 100)
+	b := databuilder.TablesBuilder{TileWidth: 1}
+	// Make a few commits, each on their own tile
 	b.CommitsWithData().
-		Insert(337, "whomever", "commit 337", "2020-12-01T00:00:01Z").
-		Insert(437, "whomever", "commit 437", "2020-12-01T00:00:02Z").
-		Insert(537, "whomever", "commit 537", "2020-12-01T00:00:03Z").
-		Insert(637, "whomever", "commit 637", "2020-12-01T00:00:04Z").
-		Insert(687, "whomever", "commit 687", "2020-12-01T00:00:05Z")
-	// Then add a bunch of empty commits after. These should not impact the latest commit/tile
-	// with data.
-	nd := b.CommitsWithNoData()
-	for i := 688; i < 710; i++ {
-		nd.Insert(i, "no data author", "no data "+strconv.Itoa(i), "2020-12-01T00:00:06Z")
-	}
+		Insert("337", "whomever", "commit 337", "2020-12-01T00:00:01Z").
+		Insert("437", "whomever", "commit 437", "2020-12-01T00:00:02Z").
+		Insert("537", "whomever", "commit 537", "2020-12-01T00:00:03Z").
+		Insert("637", "whomever", "commit 637", "2020-12-01T00:00:04Z").
+		Insert("687", "whomever", "commit 687", "2020-12-01T00:00:05Z")
 
 	b.SetDigests(map[rune]types.Digest{
 		// All of these will be untriaged because diffs don't care about triage status
@@ -549,8 +542,8 @@
 func makeDataWithIgnoredTraces() schema.Tables {
 	b := databuilder.TablesBuilder{}
 	b.CommitsWithData().
-		Insert(8, "whomever", "commit 8", "2020-12-01T00:00:01Z").
-		Append("whomever", "commit 9", "2020-12-01T00:00:02Z")
+		Insert("008", "whomever", "commit 8", "2020-12-01T00:00:01Z").
+		Insert("009", "whomever", "commit 9", "2020-12-01T00:00:02Z")
 
 	b.SetDigests(map[rune]types.Digest{
 		// All of these will be untriaged because diffs don't care about triage status
@@ -621,14 +614,14 @@
 }
 
 func newWorkerUsingImagesFromKitchenSink(t *testing.T, db *pgxpool.Pool) *WorkerImpl {
-	return New(db, &fsImageSource{root: kitchenSinkRoot(t)}, &memDiffCache{}, 2)
+	return New(db, &fsImageSource{root: kitchenSinkRoot(t)}, &memDiffCache{}, 200)
 }
 
 func newWorkerUsingBlankImages(t *testing.T, db *pgxpool.Pool) *WorkerImpl {
 	infraRoot, err := repo_root.Get()
 	require.NoError(t, err)
 	blankImagePath := filepath.Join(infraRoot, "golden", "go", "sql", "datakitchensink", "img", string(dks.DigestBlank+".png"))
-	return New(db, &fixedImageSource{img: blankImagePath}, &memDiffCache{}, 2)
+	return New(db, &fixedImageSource{img: blankImagePath}, &memDiffCache{}, 200)
 }
 
 var kitchenSinkData = dks.Build()
diff --git a/golden/go/ignore/sqlignorestore/sqlignorestore_test.go b/golden/go/ignore/sqlignorestore/sqlignorestore_test.go
index a6bb371..25c1748 100644
--- a/golden/go/ignore/sqlignorestore/sqlignorestore_test.go
+++ b/golden/go/ignore/sqlignorestore/sqlignorestore_test.go
@@ -167,7 +167,7 @@
 // a single ignore rule.
 func loadTestData(t *testing.T, ctx context.Context, db *pgxpool.Pool) {
 	data := databuilder.TablesBuilder{}
-	data.CommitsWithData().Append("whoever@example.com", "initial commit", "2021-01-11T16:00:00Z")
+	data.CommitsWithData().Insert("123", "whoever@example.com", "initial commit", "2021-01-11T16:00:00Z")
 	data.SetDigests(map[rune]types.Digest{
 		'a': datakitchensink.DigestA04Unt,
 	})
diff --git a/golden/go/sql/databuilder/databuilder.go b/golden/go/sql/databuilder/databuilder.go
index 75134d3..d8feb87 100644
--- a/golden/go/sql/databuilder/databuilder.go
+++ b/golden/go/sql/databuilder/databuilder.go
@@ -5,6 +5,7 @@
 import (
 	"bytes"
 	"crypto/md5"
+	"crypto/sha1"
 	"encoding/hex"
 	"fmt"
 	"image"
@@ -13,7 +14,6 @@
 	"io/ioutil"
 	"path/filepath"
 	"sort"
-	"strings"
 	"time"
 
 	"github.com/google/uuid"
@@ -30,15 +30,17 @@
 // TablesBuilder has methods on it for generating trace data and other related data in a way
 // that can be easily turned into SQL table rows.
 type TablesBuilder struct {
+	// TileWidth is the number of commits with data that should be grouped together in a tile.
+	// In production, tiles will default to 100 dense commits wide.
+	TileWidth int
+
 	changelistBuilders  []*ChangelistBuilder
 	commitsWithData     *CommitBuilder
-	commitsWithNoData   *CommitBuilder
 	diffMetrics         []schema.DiffMetricRow
 	expectationBuilders []*ExpectationsBuilder
 	groupingKeys        []string
 	ignoreRules         []schema.IgnoreRuleRow
 	runeToDigest        map[rune]schema.DigestBytes
-	tileWidth           int
 	traceBuilders       []*TraceBuilder
 }
 
@@ -52,15 +54,6 @@
 	return b.commitsWithData
 }
 
-// CommitsWithNoData returns a new CommitBuilder that will explicitly not be connected to data.
-func (b *TablesBuilder) CommitsWithNoData() *CommitBuilder {
-	if b.commitsWithNoData != nil {
-		logAndPanic("Cannot call Commits() more than once.")
-	}
-	b.commitsWithNoData = &CommitBuilder{}
-	return b.commitsWithNoData
-}
-
 // SetDigests loads a mapping of runes to the digest that they represent. This allows
 // specifying the trace history be done with a string of characters. If a rune is invalid or
 // the digests are invalid, this will panic. It panics if called more than once.
@@ -380,10 +373,14 @@
 // Build should be called when all the data has been loaded in for a given setup. It will generate
 // the SQL rows as represented in a schema.Tables. If any validation steps fail, it will panic.
 func (b *TablesBuilder) Build() schema.Tables {
-	if b.tileWidth == 0 {
-		b.tileWidth = 100 // default
+	if b.TileWidth == 0 {
+		b.TileWidth = 100 // default
 	}
 	var tables schema.Tables
+
+	tables.Commits = b.commitsWithData.commits
+	applyTilingToCommits(tables.Commits, b.TileWidth)
+
 	commitsWithData := map[schema.CommitID]bool{}
 	valuesAtHead := map[schema.MD5Hash]*schema.ValueAtHeadRow{}
 	for _, traceBuilder := range b.traceBuilders {
@@ -433,18 +430,15 @@
 			}
 		}
 	}
-	tables.TiledTraceDigests = b.computeTiledTraceDigests()
-	tables.PrimaryBranchParams = b.computePrimaryBranchParams()
-	tables.Commits = b.commitsWithData.commits
 	for i := range tables.Commits {
 		cid := tables.Commits[i].CommitID
 		if commitsWithData[cid] {
 			tables.Commits[i].HasData = true
 		}
 	}
-	if b.commitsWithNoData != nil {
-		tables.Commits = append(tables.Commits, b.commitsWithNoData.commits...)
-	}
+
+	tables.TiledTraceDigests = b.computeTiledTraceDigests(tables.Commits)
+	tables.PrimaryBranchParams = b.computePrimaryBranchParams(tables.Commits)
 	exp := b.finalizeExpectations()
 	for _, e := range exp {
 		tables.Expectations = append(tables.Expectations, *e)
@@ -535,6 +529,25 @@
 	return tables
 }
 
+func applyTilingToCommits(commits []schema.CommitRow, tileWidth int) {
+	// We sort the commits by CommitID in lexicographical order. By definition of CommitID, this is
+	// the order they happen in.
+	sort.Slice(commits, func(i, j int) bool {
+		return commits[i].CommitID < commits[j].CommitID
+	})
+	// We start with tile 0 and keep filling up that tile until we roll over to the next one.
+	currTile := 0
+	itemsInTile := 0
+	for i := range commits {
+		commits[i].TileID = schema.TileID(currTile)
+		itemsInTile++
+		if itemsInTile >= tileWidth {
+			currTile++
+			itemsInTile = 0
+		}
+	}
+}
+
 func addOptionIfUnique(existing []schema.OptionsRow, opt schema.OptionsRow) []schema.OptionsRow {
 	for _, existingOpt := range existing {
 		if bytes.Equal(opt.OptionsID, existingOpt.OptionsID) {
@@ -595,12 +608,12 @@
 }
 
 type tiledTraceDigest struct {
-	startCommitID schema.CommitID
-	traceID       schema.MD5Hash
-	digest        schema.MD5Hash
+	tileID  schema.TileID
+	traceID schema.MD5Hash
+	digest  schema.MD5Hash
 }
 
-func (b *TablesBuilder) computeTiledTraceDigests() []schema.TiledTraceDigestRow {
+func (b *TablesBuilder) computeTiledTraceDigests(commits []schema.CommitRow) []schema.TiledTraceDigestRow {
 	seenRows := map[tiledTraceDigest]bool{}
 	for _, builder := range b.traceBuilders {
 		for _, xtv := range builder.traceValues {
@@ -608,11 +621,11 @@
 				if tv == nil {
 					continue
 				}
-				tiledID := sql.ComputeTileStartID(tv.CommitID, b.tileWidth)
+				tiledID := getTileID(tv.CommitID, commits)
 				seenRows[tiledTraceDigest{
-					startCommitID: tiledID,
-					traceID:       sql.AsMD5Hash(tv.TraceID),
-					digest:        sql.AsMD5Hash(tv.Digest),
+					tileID:  tiledID,
+					traceID: sql.AsMD5Hash(tv.TraceID),
+					digest:  sql.AsMD5Hash(tv.Digest),
 				}] = true
 			}
 		}
@@ -624,17 +637,26 @@
 		copy(tID, row.traceID[:])
 		copy(db, row.digest[:])
 		rv = append(rv, schema.TiledTraceDigestRow{
-			StartCommitID: row.startCommitID,
-			TraceID:       tID,
-			Digest:        db,
+			TileID:  row.tileID,
+			TraceID: tID,
+			Digest:  db,
 		})
 	}
 	return rv
 }
 
+func getTileID(id schema.CommitID, commits []schema.CommitRow) schema.TileID {
+	for _, c := range commits {
+		if c.CommitID == id {
+			return c.TileID
+		}
+	}
+	panic("Could not find tile for commit " + id)
+}
+
 // computePrimaryBranchParams goes through all trace data and returns the PrimaryBranchParamRow
 // with the appropriately tiled key/value pairs that showed up in the trace keys and params.
-func (b *TablesBuilder) computePrimaryBranchParams() []schema.PrimaryBranchParamRow {
+func (b *TablesBuilder) computePrimaryBranchParams(commits []schema.CommitRow) []schema.PrimaryBranchParamRow {
 	seenRows := map[schema.PrimaryBranchParamRow]bool{}
 	for _, builder := range b.traceBuilders {
 		findTraceKeys := func(traceID schema.TraceID) paramtools.Params {
@@ -660,21 +682,21 @@
 				if tv == nil {
 					continue
 				}
-				tiledID := sql.ComputeTileStartID(tv.CommitID, b.tileWidth)
+				tiledID := getTileID(tv.CommitID, commits)
 				keys := findTraceKeys(tv.TraceID)
 				for k, v := range keys {
 					seenRows[schema.PrimaryBranchParamRow{
-						StartCommitID: tiledID,
-						Key:           k,
-						Value:         v,
+						TileID: tiledID,
+						Key:    k,
+						Value:  v,
 					}] = true
 				}
 				options := findOptions(tv.OptionsID)
 				for k, v := range options {
 					seenRows[schema.PrimaryBranchParamRow{
-						StartCommitID: tiledID,
-						Key:           k,
-						Value:         v,
+						TileID: tiledID,
+						Key:    k,
+						Value:  v,
 					}] = true
 				}
 			}
@@ -726,38 +748,26 @@
 
 // CommitBuilder has methods for easily building commit history. All methods are chainable.
 type CommitBuilder struct {
-	commits    []schema.CommitRow
-	previousID int
-}
-
-// Append adds a commit whose ID is one higher than the previous commits ID. It panics if
-// the commitTime is not formatted to RFC3339.
-func (b *CommitBuilder) Append(author, subject, commitTime string) *CommitBuilder {
-	return b.Insert(b.previousID+1, author, subject, commitTime)
+	commits []schema.CommitRow
 }
 
 // Insert adds a commit with the given data. It panics if the commitTime is not formatted to
 // RFC3339 or if the commitID is not monotonically increasing from the last one.
-func (b *CommitBuilder) Insert(commitID int, author, subject, commitTime string) *CommitBuilder {
-	if commitID <= b.previousID {
-		panic("Must insert commits in monotonically increasing order")
-	}
-	gitHash := fmt.Sprintf("%04d", commitID)
-	// A true githash is 40 hex characters, so we repeat the 4 digits of the commitID 10 times.
-	gitHash = strings.Repeat(gitHash, 10)
+func (b *CommitBuilder) Insert(commitID schema.CommitID, author, subject, commitTime string) *CommitBuilder {
+	h := sha1.Sum([]byte(commitID))
+	gitHash := hex.EncodeToString(h[:])
 	ct, err := time.Parse(time.RFC3339, commitTime)
 	if err != nil {
 		logAndPanic("Invalid time %q: %s", commitTime, err)
 	}
 	b.commits = append(b.commits, schema.CommitRow{
-		CommitID:    schema.CommitID(commitID),
+		CommitID:    commitID,
 		GitHash:     gitHash,
 		CommitTime:  ct,
 		AuthorEmail: author,
 		Subject:     subject,
 		HasData:     false,
 	})
-	b.previousID = commitID
 	return b
 }
 
diff --git a/golden/go/sql/databuilder/databuilder_test.go b/golden/go/sql/databuilder/databuilder_test.go
index 2fa8bcd..eca361d 100644
--- a/golden/go/sql/databuilder/databuilder_test.go
+++ b/golden/go/sql/databuilder/databuilder_test.go
@@ -2,6 +2,7 @@
 
 import (
 	"crypto/md5"
+	"crypto/sha1"
 	"encoding/hex"
 	"testing"
 	"time"
@@ -26,13 +27,13 @@
 func TestBuild_CalledWithValidInput_ProducesCorrectData(t *testing.T) {
 	unittest.SmallTest(t)
 
-	b := TablesBuilder{}
+	b := TablesBuilder{TileWidth: 3}
 	b.CommitsWithData().
-		Append("author_one", "subject_one", "2020-12-05T16:00:00Z").
-		Append("author_two", "subject_two", "2020-12-06T17:00:00Z").
-		Append("author_three", "subject_three", "2020-12-07T18:00:00Z").
-		Append("author_four", "subject_four", "2020-12-08T19:00:00Z")
-	b.CommitsWithNoData().Insert(5, "author_five", "no data yet", "2020-12-08T20:00:00Z")
+		Insert("001", "author_one", "subject_one", "2020-12-05T16:00:00Z").
+		Insert("002", "author_two", "subject_two", "2020-12-06T17:00:00Z").
+		Insert("003", "author_three", "subject_three", "2020-12-07T18:00:00Z").
+		Insert("004", "author_four", "subject_four", "2020-12-08T19:00:00Z").
+		Insert("005", "author_five", "no data yet", "2020-12-08T20:00:00Z")
 	b.SetDigests(map[rune]types.Digest{
 		// by convention, upper case are positively triaged, lowercase
 		// are untriaged, numbers are negative, symbols are special.
@@ -48,15 +49,15 @@
 		"color_mode":      "rgb",
 		types.CorpusField: "corpus_one",
 	}).History(
-		"AAbb",
-		"D--D",
+		"AAbb-",
+		"D--D-",
 	).Keys([]paramtools.Params{{
 		types.PrimaryKeyField: "test_one",
 	}, {
 		types.PrimaryKeyField: "test_two",
 	}}).OptionsAll(paramtools.Params{"ext": "png"}).
-		IngestedFrom([]string{"crosshatch_file1", "crosshatch_file2", "crosshatch_file3", "crosshatch_file4"},
-			[]string{"2020-12-11T10:09:00Z", "2020-12-11T10:10:00Z", "2020-12-11T10:11:00Z", "2020-12-11T10:12:13Z"})
+		IngestedFrom([]string{"crosshatch_file1", "crosshatch_file2", "crosshatch_file3", "crosshatch_file4", ""},
+			[]string{"2020-12-11T10:09:00Z", "2020-12-11T10:10:00Z", "2020-12-11T10:11:00Z", "2020-12-11T10:12:13Z", ""})
 
 	b.AddTracesWithCommonKeys(paramtools.Params{
 		"os":                  "Windows10.7",
@@ -64,11 +65,11 @@
 		"color_mode":          "rgb",
 		types.CorpusField:     "corpus_one",
 		types.PrimaryKeyField: "test_two",
-	}).History("11D-").
+	}).History("11D--").
 		Keys([]paramtools.Params{{types.PrimaryKeyField: "test_one"}}).
 		OptionsPerTrace([]paramtools.Params{{"ext": "png"}}).
-		IngestedFrom([]string{"windows_file1", "windows_file2", "windows_file3", ""},
-			[]string{"2020-12-11T14:15:00Z", "2020-12-11T15:16:00Z", "2020-12-11T16:17:00Z", ""})
+		IngestedFrom([]string{"windows_file1", "windows_file2", "windows_file3", "", ""},
+			[]string{"2020-12-11T14:15:00Z", "2020-12-11T15:16:00Z", "2020-12-11T16:17:00Z", "", ""})
 
 	b.AddTriageEvent("user_one", "2020-12-12T12:12:12Z").
 		ExpectationsForGrouping(map[string]string{
@@ -156,36 +157,41 @@
 		MatchesAnyIgnoreRule: schema.NBTrue,
 	}}, tables.Traces)
 	assert.Equal(t, []schema.CommitRow{{
-		CommitID:    1,
-		GitHash:     "0001000100010001000100010001000100010001",
+		CommitID:    "001",
+		TileID:      0,
+		GitHash:     gitHash("001"),
 		CommitTime:  time.Date(2020, time.December, 5, 16, 0, 0, 0, time.UTC),
 		AuthorEmail: "author_one",
 		Subject:     "subject_one",
 		HasData:     true,
 	}, {
-		CommitID:    2,
-		GitHash:     "0002000200020002000200020002000200020002",
+		CommitID:    "002",
+		TileID:      0,
+		GitHash:     gitHash("002"),
 		CommitTime:  time.Date(2020, time.December, 6, 17, 0, 0, 0, time.UTC),
 		AuthorEmail: "author_two",
 		Subject:     "subject_two",
 		HasData:     true,
 	}, {
-		CommitID:    3,
-		GitHash:     "0003000300030003000300030003000300030003",
+		CommitID:    "003",
+		TileID:      0,
+		GitHash:     gitHash("003"),
 		CommitTime:  time.Date(2020, time.December, 7, 18, 0, 0, 0, time.UTC),
 		AuthorEmail: "author_three",
 		Subject:     "subject_three",
 		HasData:     true,
 	}, {
-		CommitID:    4,
-		GitHash:     "0004000400040004000400040004000400040004",
+		CommitID:    "004",
+		TileID:      1,
+		GitHash:     gitHash("004"),
 		CommitTime:  time.Date(2020, time.December, 8, 19, 0, 0, 0, time.UTC),
 		AuthorEmail: "author_four",
 		Subject:     "subject_four",
 		HasData:     true,
 	}, {
-		CommitID:    5,
-		GitHash:     "0005000500050005000500050005000500050005",
+		CommitID:    "005",
+		TileID:      1,
+		GitHash:     gitHash("005"),
 		CommitTime:  time.Date(2020, time.December, 8, 20, 0, 0, 0, time.UTC),
 		AuthorEmail: "author_five",
 		Subject:     "no data yet",
@@ -198,7 +204,7 @@
 	assert.Equal(t, []schema.TraceValueRow{{
 		Shard:        0x3,
 		TraceID:      h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
-		CommitID:     1,
+		CommitID:     "001",
 		Digest:       d(t, digestA),
 		GroupingID:   testOneGroupingID,
 		OptionsID:    pngOptionsID,
@@ -206,7 +212,7 @@
 	}, {
 		Shard:        0x3,
 		TraceID:      h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
-		CommitID:     2,
+		CommitID:     "002",
 		Digest:       d(t, digestA),
 		GroupingID:   testOneGroupingID,
 		OptionsID:    pngOptionsID,
@@ -214,7 +220,7 @@
 	}, {
 		Shard:        0x3,
 		TraceID:      h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
-		CommitID:     3,
+		CommitID:     "003",
 		Digest:       d(t, digestB),
 		GroupingID:   testOneGroupingID,
 		OptionsID:    pngOptionsID,
@@ -222,7 +228,7 @@
 	}, {
 		Shard:        0x3,
 		TraceID:      h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
-		CommitID:     4,
+		CommitID:     "004",
 		Digest:       d(t, digestB),
 		GroupingID:   testOneGroupingID,
 		OptionsID:    pngOptionsID,
@@ -230,7 +236,7 @@
 	}, {
 		Shard:        0x4,
 		TraceID:      h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_two","os":"Android","source_type":"corpus_one"}`),
-		CommitID:     1,
+		CommitID:     "001",
 		Digest:       d(t, digestD),
 		GroupingID:   testTwoGroupingID,
 		OptionsID:    pngOptionsID,
@@ -238,7 +244,7 @@
 	}, {
 		Shard:        0x4,
 		TraceID:      h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_two","os":"Android","source_type":"corpus_one"}`),
-		CommitID:     4,
+		CommitID:     "004",
 		Digest:       d(t, digestD),
 		GroupingID:   testTwoGroupingID,
 		OptionsID:    pngOptionsID,
@@ -246,7 +252,7 @@
 	}, {
 		Shard:        0x6,
 		TraceID:      h(`{"color_mode":"rgb","device":"NUC1234","name":"test_two","os":"Windows10.7","source_type":"corpus_one"}`),
-		CommitID:     1,
+		CommitID:     "001",
 		Digest:       d(t, digestC),
 		GroupingID:   testTwoGroupingID,
 		OptionsID:    pngOptionsID,
@@ -254,7 +260,7 @@
 	}, {
 		Shard:        0x6,
 		TraceID:      h(`{"color_mode":"rgb","device":"NUC1234","name":"test_two","os":"Windows10.7","source_type":"corpus_one"}`),
-		CommitID:     2,
+		CommitID:     "002",
 		Digest:       d(t, digestC),
 		GroupingID:   testTwoGroupingID,
 		OptionsID:    pngOptionsID,
@@ -262,7 +268,7 @@
 	}, {
 		Shard:        0x6,
 		TraceID:      h(`{"color_mode":"rgb","device":"NUC1234","name":"test_two","os":"Windows10.7","source_type":"corpus_one"}`),
-		CommitID:     3,
+		CommitID:     "003",
 		Digest:       d(t, digestD),
 		GroupingID:   testTwoGroupingID,
 		OptionsID:    pngOptionsID,
@@ -367,40 +373,57 @@
 		Timestamp:         ts,
 	}}, tables.DiffMetrics)
 	assert.ElementsMatch(t, []schema.TiledTraceDigestRow{{
-		TraceID:       h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
-		StartCommitID: 0,
-		Digest:        d(t, digestA),
+		TraceID: h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
+		TileID:  0,
+		Digest:  d(t, digestA),
 	}, {
-		TraceID:       h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
-		StartCommitID: 0,
-		Digest:        d(t, digestB),
+		TraceID: h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
+		TileID:  0,
+		Digest:  d(t, digestB),
 	}, {
-		TraceID:       h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_two","os":"Android","source_type":"corpus_one"}`),
-		StartCommitID: 0,
-		Digest:        d(t, digestD),
+		TraceID: h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_two","os":"Android","source_type":"corpus_one"}`),
+		TileID:  0,
+		Digest:  d(t, digestD),
 	}, {
-		TraceID:       h(`{"color_mode":"rgb","device":"NUC1234","name":"test_two","os":"Windows10.7","source_type":"corpus_one"}`),
-		StartCommitID: 0,
-		Digest:        d(t, digestC),
+		TraceID: h(`{"color_mode":"rgb","device":"NUC1234","name":"test_two","os":"Windows10.7","source_type":"corpus_one"}`),
+		TileID:  0,
+		Digest:  d(t, digestC),
 	}, {
-		TraceID:       h(`{"color_mode":"rgb","device":"NUC1234","name":"test_two","os":"Windows10.7","source_type":"corpus_one"}`),
-		StartCommitID: 0,
-		Digest:        d(t, digestD),
+		TraceID: h(`{"color_mode":"rgb","device":"NUC1234","name":"test_two","os":"Windows10.7","source_type":"corpus_one"}`),
+		TileID:  0,
+		Digest:  d(t, digestD),
+	}, {
+		TraceID: h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
+		TileID:  1,
+		Digest:  d(t, digestB),
+	}, {
+		TraceID: h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_two","os":"Android","source_type":"corpus_one"}`),
+		TileID:  1,
+		Digest:  d(t, digestD),
 	}}, tables.TiledTraceDigests)
 	assert.ElementsMatch(t, []schema.PrimaryBranchParamRow{
-		{Key: "name", Value: "test_one", StartCommitID: 0},
-		{Key: "name", Value: "test_two", StartCommitID: 0},
-		{Key: "device", Value: "Crosshatch", StartCommitID: 0},
-		{Key: "device", Value: "NUC1234", StartCommitID: 0},
-		{Key: "os", Value: "Android", StartCommitID: 0},
-		{Key: "os", Value: "Windows10.7", StartCommitID: 0},
-		{Key: "color_mode", Value: "rgb", StartCommitID: 0},
-		{Key: "source_type", Value: "corpus_one", StartCommitID: 0},
-		{Key: "ext", Value: "png", StartCommitID: 0},
+		{Key: "name", Value: "test_one", TileID: 0},
+		{Key: "name", Value: "test_two", TileID: 0},
+		{Key: "device", Value: "Crosshatch", TileID: 0},
+		{Key: "device", Value: "NUC1234", TileID: 0},
+		{Key: "os", Value: "Android", TileID: 0},
+		{Key: "os", Value: "Windows10.7", TileID: 0},
+		{Key: "color_mode", Value: "rgb", TileID: 0},
+		{Key: "source_type", Value: "corpus_one", TileID: 0},
+		{Key: "ext", Value: "png", TileID: 0},
+		// Note there's no Windows 10.7 or NUC1234 key because that hasn't been generated in
+		// the second tile (starting at commit 4).
+		{Key: "name", Value: "test_one", TileID: 1},
+		{Key: "name", Value: "test_two", TileID: 1},
+		{Key: "device", Value: "Crosshatch", TileID: 1},
+		{Key: "os", Value: "Android", TileID: 1},
+		{Key: "color_mode", Value: "rgb", TileID: 1},
+		{Key: "source_type", Value: "corpus_one", TileID: 1},
+		{Key: "ext", Value: "png", TileID: 1},
 	}, tables.PrimaryBranchParams)
 	assert.ElementsMatch(t, []schema.ValueAtHeadRow{{
 		TraceID:              h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
-		MostRecentCommitID:   4,
+		MostRecentCommitID:   "004",
 		Digest:               d(t, digestB),
 		OptionsID:            pngOptionsID,
 		GroupingID:           testOneGroupingID,
@@ -411,7 +434,7 @@
 		MatchesAnyIgnoreRule: schema.NBFalse,
 	}, {
 		TraceID:              h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_two","os":"Android","source_type":"corpus_one"}`),
-		MostRecentCommitID:   4,
+		MostRecentCommitID:   "004",
 		Digest:               d(t, digestD),
 		OptionsID:            pngOptionsID,
 		GroupingID:           testTwoGroupingID,
@@ -422,7 +445,7 @@
 		MatchesAnyIgnoreRule: schema.NBFalse,
 	}, {
 		TraceID:              h(`{"color_mode":"rgb","device":"NUC1234","name":"test_two","os":"Windows10.7","source_type":"corpus_one"}`),
-		MostRecentCommitID:   3,
+		MostRecentCommitID:   "003",
 		Digest:               d(t, digestD),
 		OptionsID:            pngOptionsID,
 		GroupingID:           testTwoGroupingID,
@@ -454,7 +477,7 @@
 
 	b := TablesBuilder{}
 	b.CommitsWithData().
-		Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+		Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	b.SetDigests(map[rune]types.Digest{
 		// by convention, upper case are positively triaged, lowercase
 		// are untriaged, numbers are negative, symbols are special.
@@ -591,7 +614,7 @@
 	assert.Equal(t, []schema.TraceValueRow{{
 		Shard:        0x3,
 		TraceID:      h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_one","os":"Android","source_type":"corpus_one"}`),
-		CommitID:     1,
+		CommitID:     "123",
 		Digest:       d(t, digestA),
 		GroupingID:   h(`{"name":"test_one","source_type":"corpus_one"}`),
 		OptionsID:    h(`{"ext":"png"}`),
@@ -599,20 +622,20 @@
 	}, {
 		Shard:        0x4,
 		TraceID:      h(`{"color_mode":"rgb","device":"Crosshatch","name":"test_two","os":"Android","source_type":"corpus_one"}`),
-		CommitID:     1,
+		CommitID:     "123",
 		Digest:       d(t, digestD),
 		GroupingID:   h(`{"name":"test_two","source_type":"corpus_one"}`),
 		OptionsID:    h(`{"ext":"png"}`),
 		SourceFileID: h("crosshatch_file1"),
 	}}, tables.TraceValues)
 	assert.ElementsMatch(t, []schema.PrimaryBranchParamRow{
-		{Key: "name", Value: "test_one", StartCommitID: 0},
-		{Key: "name", Value: "test_two", StartCommitID: 0},
-		{Key: "device", Value: "Crosshatch", StartCommitID: 0},
-		{Key: "os", Value: "Android", StartCommitID: 0},
-		{Key: "color_mode", Value: "rgb", StartCommitID: 0},
-		{Key: "source_type", Value: "corpus_one", StartCommitID: 0},
-		{Key: "ext", Value: "png", StartCommitID: 0},
+		{Key: "name", Value: "test_one", TileID: 0},
+		{Key: "name", Value: "test_two", TileID: 0},
+		{Key: "device", Value: "Crosshatch", TileID: 0},
+		{Key: "os", Value: "Android", TileID: 0},
+		{Key: "color_mode", Value: "rgb", TileID: 0},
+		{Key: "source_type", Value: "corpus_one", TileID: 0},
+		{Key: "ext", Value: "png", TileID: 0},
 	}, tables.PrimaryBranchParams)
 	qualifiedCLID := "gerrit_changelist_one"
 	assert.Equal(t, []schema.ChangelistRow{{
@@ -860,36 +883,49 @@
 
 	b := TablesBuilder{}
 	assert.Panics(t, func() {
-		b.CommitsWithData().Append("fine", "dandy", "no good")
+		b.CommitsWithData().Insert("fine", "dandy", "bueno", "no good")
 	})
 }
 
-func TestCommits_InsertInMonotonicOrder_Success(t *testing.T) {
+func TestCommits_InsertInAnyOrder_Success(t *testing.T) {
 	unittest.SmallTest(t)
 
-	b := TablesBuilder{}
+	b := TablesBuilder{TileWidth: 2}
 	b.CommitsWithData().
-		Insert(98, "author_one", "subject_98", "2020-12-05T15:00:00Z").
-		Append("author_one", "subject_99", "2020-12-05T16:00:00Z").
-		Insert(2000, "author_2k", "subject_2k", "2022-02-02T02:02:00Z")
+		Insert("0100", "author_100", "subject_100", "2021-01-01T01:01:00Z").
+		Insert("0099", "author_one", "subject_99", "2020-12-05T15:00:00Z").
+		Insert("0098", "author_two", "subject_98", "2020-12-05T14:00:00Z").
+		Insert("2000", "author_2k", "subject_2k", "2022-02-02T02:02:00Z")
+
 	tables := b.Build()
 	assert.Equal(t, []schema.CommitRow{{
-		CommitID:    98,
-		GitHash:     "0098009800980098009800980098009800980098",
-		CommitTime:  time.Date(2020, time.December, 5, 15, 0, 0, 0, time.UTC),
-		AuthorEmail: "author_one",
+		CommitID:    "0098",
+		TileID:      0,
+		GitHash:     gitHash("0098"),
+		CommitTime:  time.Date(2020, time.December, 5, 14, 0, 0, 0, time.UTC),
+		AuthorEmail: "author_two",
 		Subject:     "subject_98",
 		HasData:     false,
 	}, {
-		CommitID:    99,
-		GitHash:     "0099009900990099009900990099009900990099",
-		CommitTime:  time.Date(2020, time.December, 5, 16, 0, 0, 0, time.UTC),
+		CommitID:    "0099",
+		TileID:      0,
+		GitHash:     gitHash("0099"),
+		CommitTime:  time.Date(2020, time.December, 5, 15, 0, 0, 0, time.UTC),
 		AuthorEmail: "author_one",
 		Subject:     "subject_99",
 		HasData:     false,
 	}, {
-		CommitID:    2000,
-		GitHash:     "2000200020002000200020002000200020002000",
+		CommitID:    "0100",
+		TileID:      1,
+		GitHash:     gitHash("0100"),
+		CommitTime:  time.Date(2021, time.January, 1, 1, 1, 0, 0, time.UTC),
+		AuthorEmail: "author_100",
+		Subject:     "subject_100",
+		HasData:     false,
+	}, {
+		CommitID:    "2000",
+		TileID:      1,
+		GitHash:     gitHash("2000"),
 		CommitTime:  time.Date(2022, time.February, 2, 2, 2, 0, 0, time.UTC),
 		AuthorEmail: "author_2k",
 		Subject:     "subject_2k",
@@ -897,18 +933,6 @@
 	}}, tables.Commits)
 }
 
-func TestCommits_InsertOutOfOrder_Panics(t *testing.T) {
-	unittest.SmallTest(t)
-
-	b := TablesBuilder{}
-	assert.Panics(t, func() {
-		b.CommitsWithData().
-			Insert(2000, "author_2k", "subject_2k", "2022-02-02T02:02:00Z").
-			Insert(98, "author_one", "subject_98", "2020-12-05T15:00:00Z").
-			Append("author_one", "subject_99", "2020-12-05T16:00:00Z")
-	})
-}
-
 func TestSetDigests_CalledMultipleTimes_Panics(t *testing.T) {
 	unittest.SmallTest(t)
 
@@ -949,7 +973,7 @@
 	assert.Panics(t, func() {
 		b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	assert.Panics(t, func() {
 		b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	})
@@ -982,7 +1006,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A")
 	assert.Panics(t, func() {
@@ -996,7 +1020,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	// Expected length is 1
 	assert.Panics(t, func() {
@@ -1017,7 +1041,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	assert.Panics(t, func() {
 		tb.History("?")
@@ -1030,7 +1054,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	assert.Panics(t, func() {
 		tb.Keys([]paramtools.Params{{types.CorpusField: "whatever"}})
@@ -1043,7 +1067,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A")
 	tb.Keys([]paramtools.Params{{types.CorpusField: "whatever"}})
@@ -1058,7 +1082,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A")
 	assert.Panics(t, func() {
@@ -1082,7 +1106,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys("group1", "group2")
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A")
 	assert.Panics(t, func() {
@@ -1097,7 +1121,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys("group1")
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A", "-")
 	assert.Panics(t, func() {
@@ -1111,7 +1135,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	assert.Panics(t, func() {
 		tb.OptionsPerTrace([]paramtools.Params{{"opt": "whatever"}})
@@ -1124,7 +1148,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A")
 	tb.OptionsPerTrace([]paramtools.Params{{"opt": "whatever"}})
@@ -1139,7 +1163,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A")
 	assert.Panics(t, func() {
@@ -1164,8 +1188,8 @@
 	b.SetGroupingKeys("test")
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
 	b.CommitsWithData().
-		Append("author_one", "subject_one", "2020-12-05T16:00:00Z").
-		Append("author_one", "subject_two", "2020-12-05T17:00:00Z")
+		Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z").
+		Insert("128", "author_one", "subject_two", "2020-12-05T17:00:00Z")
 	b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"}).
 		History("AA", "AA").
 		Keys([]paramtools.Params{{"test": "one"}, {"test": "two"}}).
@@ -1199,7 +1223,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	assert.Panics(t, func() {
 		tb.IngestedFrom([]string{"file1"}, []string{"2020-12-05T16:00:00Z"})
@@ -1212,7 +1236,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A")
 	tb.IngestedFrom([]string{"file1"}, []string{"2020-12-05T16:00:00Z"})
@@ -1227,7 +1251,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A")
 	assert.Panics(t, func() {
@@ -1256,7 +1280,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A")
 	assert.Panics(t, func() {
@@ -1270,7 +1294,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	tb := b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"})
 	tb.History("A")
 	assert.Panics(t, func() {
@@ -1297,7 +1321,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"}).
 		History("A").
 		Keys([]paramtools.Params{{types.CorpusField: "identical"}}).
@@ -1338,7 +1362,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys("test")
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"}).
 		History("A").Keys([]paramtools.Params{{"test": "one"}}).
 		OptionsAll(paramtools.Params{"opt": "opt"}).
@@ -1423,7 +1447,7 @@
 	assert.Panics(t, func() {
 		b.ComputeDiffMetricsFromImages(testDir, "2020-12-05T16:00:00Z")
 	})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"}).
 		History("A")
 	// We should have the right data now.
@@ -1439,7 +1463,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"}).
 		History("A")
 	assert.Panics(t, func() {
@@ -1453,7 +1477,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys(types.CorpusField)
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"}).
 		History("A")
 	assert.Panics(t, func() {
@@ -1489,7 +1513,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys("test")
 	b.SetDigests(map[rune]types.Digest{'A': digestA})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"}).
 		History("A").Keys([]paramtools.Params{{"test": "one"}}).
 		OptionsAll(paramtools.Params{"opt": "opt"}).
@@ -1548,7 +1572,7 @@
 	b := TablesBuilder{}
 	b.SetGroupingKeys("test")
 	b.SetDigests(map[rune]types.Digest{'B': digestB})
-	b.CommitsWithData().Append("author_one", "subject_one", "2020-12-05T16:00:00Z")
+	b.CommitsWithData().Insert("123", "author_one", "subject_one", "2020-12-05T16:00:00Z")
 	b.AddTracesWithCommonKeys(paramtools.Params{"os": "Android"}).
 		History("B").Keys([]paramtools.Params{{"test": "one"}}).
 		OptionsAll(paramtools.Params{"opt": "opt"}).
@@ -1612,3 +1636,9 @@
 	require.NoError(t, err)
 	return b
 }
+
+// The generated gitHash is simply the sha1 sum of the commit id.
+func gitHash(cID schema.CommitID) string {
+	h := sha1.Sum([]byte(cID))
+	return hex.EncodeToString(h[:])
+}
diff --git a/golden/go/sql/datakitchensink/kitchensink.go b/golden/go/sql/datakitchensink/kitchensink.go
index 0a3cd41..6e42877 100644
--- a/golden/go/sql/datakitchensink/kitchensink.go
+++ b/golden/go/sql/datakitchensink/kitchensink.go
@@ -13,25 +13,25 @@
 
 // Build creates a set of data that covers many common testing scenarios.
 func Build() schema.Tables {
-	b := databuilder.TablesBuilder{}
+	b := databuilder.TablesBuilder{TileWidth: 5}
 	// This data set has data on 10 commits and no data on 3 commits in the middle.
-	// Intentionally put these commits such that they straddle a tile (100 commits).
+	// Intentionally put these commits such that they straddle a tile.
 	// Commits with ids 103-105 have no data to help test sparse data.
 	b.CommitsWithData().
-		Insert(98, UserOne, "commit 98", "2020-12-01T00:00:00Z").
-		Append(UserTwo, "commit 99", "2020-12-02T00:00:00Z").
-		Append(UserThree, "commit 100", "2020-12-03T00:00:00Z").
-		Append(UserTwo, "Update Windows 10.2 to 10.3", "2020-12-04T00:00:00Z").
-		Append(UserOne, "commit 102", "2020-12-05T00:00:00Z").
-		Insert(106, UserTwo, "Add walleye device", "2020-12-07T00:00:00Z").
-		Append(UserThree, "Add taimen device [flaky]", "2020-12-08T00:00:00Z").
-		Append(UserTwo, "Fix iOS Triangle tests [accidental break of circle tests]", "2020-12-09T00:00:00Z").
-		Append(UserOne, "Enable autotriage of walleye", "2020-12-10T00:00:00Z").
-		Append(UserTwo, "commit 110", "2020-12-11T00:00:00Z")
-	b.CommitsWithNoData().
-		Insert(103, UserFour, "no data 103", "2020-12-06T01:00:00Z").
-		Append(UserFour, "no data 104", "2020-12-06T02:00:00Z").
-		Append(UserFour, "no data 105", "2020-12-06T03:00:00Z")
+		Insert("0000000098", UserOne, "commit 98", "2020-12-01T00:00:00Z").
+		Insert("0000000099", UserTwo, "commit 99", "2020-12-02T00:00:00Z").
+		Insert("0000000100", UserThree, "commit 100", "2020-12-03T00:00:00Z").
+		Insert("0000000101", UserTwo, "Update Windows 10.2 to 10.3", "2020-12-04T00:00:00Z").
+		Insert("0000000102", UserOne, "commit 102", "2020-12-05T00:00:00Z").
+		// TODO(kjlubick) bring this back with GitCommits table.
+		//Insert("0000000103", UserFour, "no data 103", "2020-12-06T01:00:00Z").
+		//Insert("0000000104", UserFour, "no data 104", "2020-12-06T02:00:00Z").
+		//Insert("0000000105", UserFour, "no data 105", "2020-12-06T03:00:00Z").
+		Insert("0000000106", UserTwo, "Add walleye device", "2020-12-07T00:00:00Z").
+		Insert("0000000107", UserThree, "Add taimen device [flaky]", "2020-12-08T00:00:00Z").
+		Insert("0000000108", UserTwo, "Fix iOS Triangle tests [accidental break of circle tests]", "2020-12-09T00:00:00Z").
+		Insert("0000000109", UserOne, "Enable autotriage of walleye", "2020-12-10T00:00:00Z").
+		Insert("0000000110", UserTwo, "commit 110", "2020-12-11T00:00:00Z")
 
 	b.SetDigests(map[rune]types.Digest{
 		// by convention, upper case are positively triaged, lowercase
diff --git a/golden/go/sql/schema/sql.go b/golden/go/sql/schema/sql.go
index 5c4a7d7..1498d6a 100644
--- a/golden/go/sql/schema/sql.go
+++ b/golden/go/sql/schema/sql.go
@@ -13,7 +13,8 @@
   INDEX system_status_ingested_idx (system, status, last_ingested_data)
 );
 CREATE TABLE IF NOT EXISTS Commits (
-  commit_id INT4 PRIMARY KEY,
+  commit_id STRING PRIMARY KEY,
+  tile_id INT4 NOT NULL,
   git_hash STRING NOT NULL,
   commit_time TIMESTAMP WITH TIME ZONE NOT NULL,
   author_email STRING NOT NULL,
@@ -81,10 +82,10 @@
   INDEX cl_order_idx (changelist_id, ps_order)
 );
 CREATE TABLE IF NOT EXISTS PrimaryBranchParams (
-  start_commit_id INT4,
+  tile_id INT4,
   key STRING,
   value STRING,
-  PRIMARY KEY (start_commit_id, key, value)
+  PRIMARY KEY (tile_id, key, value)
 );
 CREATE TABLE IF NOT EXISTS ProblemImages (
   digest STRING PRIMARY KEY,
@@ -125,14 +126,14 @@
 );
 CREATE TABLE IF NOT EXISTS TiledTraceDigests (
   trace_id BYTES,
-  start_commit_id INT4,
+  tile_id INT4,
   digest BYTES NOT NULL,
-  PRIMARY KEY (trace_id, start_commit_id, digest)
+  PRIMARY KEY (trace_id, tile_id, digest)
 );
 CREATE TABLE IF NOT EXISTS TraceValues (
   shard INT2,
   trace_id BYTES,
-  commit_id INT4,
+  commit_id STRING,
   digest BYTES NOT NULL,
   grouping_id BYTES NOT NULL,
   options_id BYTES NOT NULL,
@@ -157,7 +158,7 @@
 );
 CREATE TABLE IF NOT EXISTS ValuesAtHead (
   trace_id BYTES PRIMARY KEY,
-  most_recent_commit_id INT4 NOT NULL,
+  most_recent_commit_id STRING NOT NULL,
   digest BYTES NOT NULL,
   options_id BYTES NOT NULL,
   grouping_id BYTES NOT NULL,
diff --git a/golden/go/sql/schema/tables.go b/golden/go/sql/schema/tables.go
index 853cf22..4a9734d 100644
--- a/golden/go/sql/schema/tables.go
+++ b/golden/go/sql/schema/tables.go
@@ -9,12 +9,6 @@
 	"go.skia.org/infra/go/paramtools"
 )
 
-// TileWidth is the number of sparse commits whose data is grouped together when creating
-// the PrimaryBranchParams and TiledTraceDigests tables. This should speed up queries that would
-// otherwise have to look at potentially 100x as much data. For data that is very dense, this
-// might not help as much, so we might need to have instance-dependent widths.
-const TileWidth = 100
-
 // MD5Hash is a specialized type for an array of bytes representing an MD5Hash. We use MD5 hashes
 // a lot because they are a deterministic way to generate primary keys, which allows us to more
 // easily cache and deduplicate data when ingesting. Additionally, these hashes are somewhat
@@ -33,7 +27,13 @@
 type DigestBytes []byte
 type SourceFileID []byte
 
-type CommitID int32
+// CommitID is responsible for indicating when a commit happens in time. CommitIDs should be
+// treated as ordered lexicographically, but need not be densely populated.
+type CommitID string
+
+// TileID is a mechanism for batching together data to speed up queries. A tile will consist of a
+// configurable number of commits with data.
+type TileID int
 
 type NullableBool int
 
@@ -115,7 +115,7 @@
 	TraceID TraceID `sql:"trace_id BYTES"`
 	// CommitID represents when in time this data was drawn. This is a foreign key into the
 	// CommitIDs table.
-	CommitID CommitID `sql:"commit_id INT4"`
+	CommitID CommitID `sql:"commit_id STRING"`
 	// Digest is the MD5 hash of the pixel data; this is "what was drawn" at this point in time
 	// (specified by CommitID) and by the machine (specified by TraceID).
 	Digest DigestBytes `sql:"digest BYTES NOT NULL"`
@@ -143,8 +143,18 @@
 }
 
 type CommitRow struct {
-	// CommitID is a monotonically increasing number as we follow the primary repo through time.
-	CommitID CommitID `sql:"commit_id INT4 PRIMARY KEY"`
+	// CommitID is a potentially arbitrary string. commit_ids will be treated as occurring in
+	// lexicographical order.
+	CommitID CommitID `sql:"commit_id STRING PRIMARY KEY"`
+	// TileID is an integer that corresponds to the tile for which this commit belongs. Tiles are
+	// intended to be about 100 commits wide, but this could vary slightly if commits are ingested
+	// in not quite sequential order. Additionally, tile widths could change over time if necessary.
+	// It is expected that tile_id be set the first time we see data from a given commit on the
+	// primary branch and not changed after, even if the tile size used for an instance changes.
+	TileID TileID `sql:"tile_id INT4 NOT NULL"`
+
+	// TODO(kjlubick) split the git stuff into a GitCommits table.
+
 	// GitHash is the git hash of the commit.
 	GitHash string `sql:"git_hash STRING NOT NULL"`
 	// CommitTime is the timestamp associated with the commit.
@@ -162,8 +172,8 @@
 
 // ToSQLRow implements the sqltest.SQLExporter interface.
 func (r CommitRow) ToSQLRow() (colNames []string, colData []interface{}) {
-	return []string{"commit_id", "git_hash", "commit_time", "author_email", "subject", "has_data"},
-		[]interface{}{r.CommitID, r.GitHash, r.CommitTime, r.AuthorEmail, r.Subject, r.HasData}
+	return []string{"commit_id", "tile_id", "git_hash", "commit_time", "author_email", "subject", "has_data"},
+		[]interface{}{r.CommitID, r.TileID, r.GitHash, r.CommitTime, r.AuthorEmail, r.Subject, r.HasData}
 }
 
 type TraceRow struct {
@@ -377,7 +387,7 @@
 	TraceID TraceID `sql:"trace_id BYTES PRIMARY KEY"`
 	// MostRecentCommitID represents when in time this data was drawn. This is a foreign key into
 	// the CommitIDs table.
-	MostRecentCommitID CommitID `sql:"most_recent_commit_id INT4 NOT NULL"`
+	MostRecentCommitID CommitID `sql:"most_recent_commit_id STRING NOT NULL"`
 	// Digest is the MD5 hash of the pixel data; this is "what was drawn" at this point in time
 	// (specified by MostRecentCommitID) and by the machine (specified by TraceID).
 	Digest DigestBytes `sql:"digest BYTES NOT NULL"`
@@ -418,23 +428,21 @@
 // contains Keys *and* Options because users can search/filter/query by both of those and this table
 // is used to fill in the UI widgets with the available search options.
 type PrimaryBranchParamRow struct {
-	// StartCommitID is the commit id that is the beginning of the tile for which this row
-	// corresponds. For example, with a tile width of 100, data from commit 73 would correspond to
-	// StartCommitID == 0; data from commit 1234 would correspond with StartCommitID == 1200 and
-	// so on. This is a foreign key into the CommitIDs table.
-	StartCommitID CommitID `sql:"start_commit_id INT4"`
+	// TileID indicates which tile the given Key and Value were seen on in the primary branch.
+	// This is a foreign key into the Commits table.
+	TileID TileID `sql:"tile_id INT4"`
 	// Key is the key of a trace key or option.
 	Key string `sql:"key STRING"`
 	// Value is the value associated with the key.
 	Value string `sql:"value STRING"`
 	// We generally want locality by tile, so that goes first in the primary key.
-	primaryKey struct{} `sql:"PRIMARY KEY (start_commit_id, key, value)"`
+	primaryKey struct{} `sql:"PRIMARY KEY (tile_id, key, value)"`
 }
 
 // ToSQLRow implements the sqltest.SQLExporter interface.
 func (r PrimaryBranchParamRow) ToSQLRow() (colNames []string, colData []interface{}) {
-	return []string{"start_commit_id", "key", "value"},
-		[]interface{}{r.StartCommitID, r.Key, r.Value}
+	return []string{"tile_id", "key", "value"},
+		[]interface{}{r.TileID, r.Key, r.Value}
 }
 
 // TiledTraceDigestRow corresponds to a given trace producing a given digest within a range (tile)
@@ -446,18 +454,18 @@
 	TraceID TraceID `sql:"trace_id BYTES"`
 	// StartCommitID is the commit id that is the beginning of the tile for which this row
 	// corresponds.
-	StartCommitID CommitID `sql:"start_commit_id INT4"`
+	TileID TileID `sql:"tile_id INT4"`
 	// Digest is the MD5 hash of the pixel data; this is "what was drawn" at least once in the tile
 	// specified by StartCommitID and by the machine (specified by TraceID).
 	Digest DigestBytes `sql:"digest BYTES NOT NULL"`
 	// We generally want locality by TraceID, so that goes first in the primary key.
-	primaryKey struct{} `sql:"PRIMARY KEY (trace_id, start_commit_id, digest)"`
+	primaryKey struct{} `sql:"PRIMARY KEY (trace_id, tile_id, digest)"`
 }
 
 // ToSQLRow implements the sqltest.SQLExporter interface.
 func (r TiledTraceDigestRow) ToSQLRow() (colNames []string, colData []interface{}) {
-	return []string{"trace_id", "start_commit_id", "digest"},
-		[]interface{}{r.TraceID, r.StartCommitID, r.Digest}
+	return []string{"trace_id", "tile_id", "digest"},
+		[]interface{}{r.TraceID, r.TileID, r.Digest}
 }
 
 type IgnoreRuleRow struct {
diff --git a/golden/go/sql/util.go b/golden/go/sql/util.go
index 1ad6d69..1718f2f 100644
--- a/golden/go/sql/util.go
+++ b/golden/go/sql/util.go
@@ -50,11 +50,6 @@
 	return traceID[0] % TraceValuesShards
 }
 
-// ComputeTileStartID returns the commit id is the beginning of the tile that the commit is in.
-func ComputeTileStartID(cid schema.CommitID, tileWidth int) schema.CommitID {
-	return (cid / schema.CommitID(tileWidth)) * schema.CommitID(tileWidth)
-}
-
 // AsMD5Hash returns the given byte slice as an MD5Hash (for easier use with maps)
 func AsMD5Hash(b []byte) schema.MD5Hash {
 	var m schema.MD5Hash
diff --git a/golden/go/sql/util_test.go b/golden/go/sql/util_test.go
index 1492213..c186542 100644
--- a/golden/go/sql/util_test.go
+++ b/golden/go/sql/util_test.go
@@ -75,18 +75,6 @@
 	assert.Equal(t, byte(0x03), ComputeTraceValueShard(schema.TraceID{0x13, 0x14}))
 }
 
-func TestComputeTileStartID_Success(t *testing.T) {
-	unittest.SmallTest(t)
-
-	assert.Equal(t, schema.CommitID(0), ComputeTileStartID(87, 100))
-	assert.Equal(t, schema.CommitID(100), ComputeTileStartID(127, 100))
-	assert.Equal(t, schema.CommitID(1200), ComputeTileStartID(1234, 100))
-
-	assert.Equal(t, schema.CommitID(0), ComputeTileStartID(87, 500))
-	assert.Equal(t, schema.CommitID(0), ComputeTileStartID(127, 500))
-	assert.Equal(t, schema.CommitID(1000), ComputeTileStartID(1234, 500))
-}
-
 func TestAsMD5Hash_Success(t *testing.T) {
 	unittest.SmallTest(t)
 
diff --git a/golden/k8s-instances/angle/angle-diffcalculator.json5 b/golden/k8s-instances/angle/angle-diffcalculator.json5
index 73c0d46..42944c2 100644
--- a/golden/k8s-instances/angle/angle-diffcalculator.json5
+++ b/golden/k8s-instances/angle/angle-diffcalculator.json5
@@ -1,10 +1,10 @@
 {
+  commits_with_data_to_search: 200,
   diff_cache_namespace: "angle",
   diff_work_subscription: "gold-angle-diffcalculator", // includes instance id
   memcached_server: "gold-memcached-0.gold-memcached:11211",
   prom_port: ":20000",
   ready_port: ":8000",
-  tiles_to_process: 2,
 
   // These values affect the k8s deployment; they are not read in by the binary.
   K8S_REPLICAS: 1,
diff --git a/golden/k8s-instances/chrome/chrome-diffcalculator.json5 b/golden/k8s-instances/chrome/chrome-diffcalculator.json5
index c175ace..0d15e49 100644
--- a/golden/k8s-instances/chrome/chrome-diffcalculator.json5
+++ b/golden/k8s-instances/chrome/chrome-diffcalculator.json5
@@ -1,11 +1,11 @@
 {
+  commits_with_data_to_search: 200,
   diff_cache_namespace: "chrome",
   diff_work_subscription: "gold-chrome-diffcalculator", // includes instance id
   memcached_server: "gold-memcached-0.gold-memcached:11211",
   prom_port: ":20000",
   pubsub_fetch_size: 100,
   ready_port: ":8000",
-  tiles_to_process: 2,
 
   // These values affect the k8s deployment; they are not read in by the binary.
   K8S_REPLICAS: 4,
diff --git a/golden/k8s-instances/cros-tast-dev/cros-tast-dev-diffcalculator.json5 b/golden/k8s-instances/cros-tast-dev/cros-tast-dev-diffcalculator.json5
index 9e86d30..c4ac306 100644
--- a/golden/k8s-instances/cros-tast-dev/cros-tast-dev-diffcalculator.json5
+++ b/golden/k8s-instances/cros-tast-dev/cros-tast-dev-diffcalculator.json5
@@ -1,10 +1,10 @@
 {
+  commits_with_data_to_search: 200,
   diff_cache_namespace: "cros-tast-dev",
   diff_work_subscription: "gold-cros-tast-dev-diffcalculator", // includes instance id
   memcached_server: "gold-memcached-0.gold-memcached:11211",
   prom_port: ":20000",
   ready_port: ":8000",
-  tiles_to_process: 2,
 
   // These values affect the k8s deployment; they are not read in by the binary.
   K8S_REPLICAS: 1,
diff --git a/golden/k8s-instances/flutter-engine/flutter-engine-diffcalculator.json5 b/golden/k8s-instances/flutter-engine/flutter-engine-diffcalculator.json5
index b5e3cbe..f32323b 100644
--- a/golden/k8s-instances/flutter-engine/flutter-engine-diffcalculator.json5
+++ b/golden/k8s-instances/flutter-engine/flutter-engine-diffcalculator.json5
@@ -1,10 +1,10 @@
 {
+  commits_with_data_to_search: 200,
   diff_cache_namespace: "flutter-engine",
   diff_work_subscription: "gold-flutter-engine-diffcalculator", // includes instance id
   memcached_server: "gold-memcached-0.gold-memcached:11211",
   prom_port: ":20000",
   ready_port: ":8000",
-  tiles_to_process: 2,
 
   // These values affect the k8s deployment; they are not read in by the binary.
   K8S_REPLICAS: 1,
diff --git a/golden/k8s-instances/flutter/flutter-diffcalculator.json5 b/golden/k8s-instances/flutter/flutter-diffcalculator.json5
index 2385bf2..fc75464 100644
--- a/golden/k8s-instances/flutter/flutter-diffcalculator.json5
+++ b/golden/k8s-instances/flutter/flutter-diffcalculator.json5
@@ -1,10 +1,10 @@
 {
+  commits_with_data_to_search: 200,
   diff_cache_namespace: "flutter",
   diff_work_subscription: "gold-flutter-diffcalculator", // includes instance id
   memcached_server: "gold-memcached-0.gold-memcached:11211",
   prom_port: ":20000",
   ready_port: ":8000",
-  tiles_to_process: 2,
 
   // These values affect the k8s deployment; they are not read in by the binary.
   K8S_REPLICAS: 2,
diff --git a/golden/k8s-instances/fuchsia-public/fuchsia-public-diffcalculator.json5 b/golden/k8s-instances/fuchsia-public/fuchsia-public-diffcalculator.json5
index dfd6df5..b431c57 100644
--- a/golden/k8s-instances/fuchsia-public/fuchsia-public-diffcalculator.json5
+++ b/golden/k8s-instances/fuchsia-public/fuchsia-public-diffcalculator.json5
@@ -1,10 +1,10 @@
 {
+  commits_with_data_to_search: 200,
   diff_cache_namespace: "fuchsia-public",
   diff_work_subscription: "gold-fuchsia-public-diffcalculator", // includes instance id
   memcached_server: "gold-memcached-0.gold-memcached:11211",
   prom_port: ":20000",
   ready_port: ":8000",
-  tiles_to_process: 2,
 
   // These values affect the k8s deployment; they are not read in by the binary.
   K8S_REPLICAS: 1,
diff --git a/golden/k8s-instances/lottie/lottie-diffcalculator.json5 b/golden/k8s-instances/lottie/lottie-diffcalculator.json5
index 3ff4ea3..022ccf7 100644
--- a/golden/k8s-instances/lottie/lottie-diffcalculator.json5
+++ b/golden/k8s-instances/lottie/lottie-diffcalculator.json5
@@ -1,10 +1,10 @@
 {
+  commits_with_data_to_search: 200,
   diff_cache_namespace: "lottie",
   diff_work_subscription: "gold-lottie-diffcalculator", // includes instance id
   memcached_server: "gold-memcached-0.gold-memcached:11211",
   prom_port: ":20000",
   ready_port: ":8000",
-  tiles_to_process: 2,
 
   // These values affect the k8s deployment; they are not read in by the binary.
   K8S_REPLICAS: 1,
diff --git a/golden/k8s-instances/pdfium/pdfium-diffcalculator.json5 b/golden/k8s-instances/pdfium/pdfium-diffcalculator.json5
index d57f287..b1e779b 100644
--- a/golden/k8s-instances/pdfium/pdfium-diffcalculator.json5
+++ b/golden/k8s-instances/pdfium/pdfium-diffcalculator.json5
@@ -1,10 +1,10 @@
 {
+  commits_with_data_to_search: 200,
   diff_cache_namespace: "pdfium",
   diff_work_subscription: "gold-pdfium-diffcalculator", // includes instance id
   memcached_server: "gold-memcached-0.gold-memcached:11211",
   prom_port: ":20000",
   ready_port: ":8000",
-  tiles_to_process: 2,
 
   // These values affect the k8s deployment; they are not read in by the binary.
   K8S_REPLICAS: 1,
diff --git a/golden/k8s-instances/skia-infra/skia-infra-diffcalculator.json5 b/golden/k8s-instances/skia-infra/skia-infra-diffcalculator.json5
index f7c0e77..ec13cce 100644
--- a/golden/k8s-instances/skia-infra/skia-infra-diffcalculator.json5
+++ b/golden/k8s-instances/skia-infra/skia-infra-diffcalculator.json5
@@ -1,10 +1,10 @@
 {
+  commits_with_data_to_search: 200,
   diff_cache_namespace: "skia-infra",
   diff_work_subscription: "gold-skia-infra-diffcalculator", // includes instance id
   memcached_server: "gold-memcached-0.gold-memcached:11211",
   prom_port: ":20000",
   ready_port: ":8000",
-  tiles_to_process: 2,
 
   // These values affect the k8s deployment; they are not read in by the binary.
   K8S_REPLICAS: 1,
diff --git a/golden/k8s-instances/skia/skia-diffcalculator.json5 b/golden/k8s-instances/skia/skia-diffcalculator.json5
index d813cc6..c20d36a 100644
--- a/golden/k8s-instances/skia/skia-diffcalculator.json5
+++ b/golden/k8s-instances/skia/skia-diffcalculator.json5
@@ -1,10 +1,10 @@
 {
+  commits_with_data_to_search: 200,
   diff_cache_namespace: "skia",
   diff_work_subscription: "gold-skia-diffcalculator", // includes instance id
   memcached_server: "gold-memcached-0.gold-memcached:11211",
   prom_port: ":20000",
   ready_port: ":8000",
-  tiles_to_process: 2,
 
   // These values affect the k8s deployment; they are not read in by the binary.
   K8S_REPLICAS: 12,