Repository: vitessio/vitess
Pull request #5509 (2019-12-05 07:04:18)

vindex & vrepl: multi-column support
This new change extends the Vindex protocol slightly differently
from the previous approach.

In the previous approach, a Vindex had the option of supporting
an additional MultiColumn Map API on top of the existing single
column Map. This allowed for a Vindex to be a functional one
through the MultiColumn API, and a lookup one through the regular
Map.

But the same functionality can be achieved by two vindexes, and
leads to a more flexible and composable design.

In the new design, a Vindex can decide if it wants to provide
a SingleColumn of MultiColumn API. If the caller is capable of
using the MultiColumn API, they use it. Otherwise, they treat
the vindex as non-existent.

This is the initial cut. With this change, we can bring back
the restriction to disallow owned vindexes from being primary.

Insert and VReplication use MultiColumn. The rest of v3
will continue to use SingleColumn for now.



Patch:
diff --git a/go/vt/binlog/keyspace_id_resolver.go b/go/vt/binlog/keyspace_id_resolver.go
index 204960f06c..a005222434 100644
--- a/go/vt/binlog/keyspace_id_resolver.go
+++ b/go/vt/binlog/keyspace_id_resolver.go
@@ -147,7 +147,8 @@ func newKeyspaceIDResolverFactoryV3(ctx context.Context, ts *topo.Server, keyspa
 			if col.Name.EqualString(shardingColumnName) {
 				// We found the column.
 				return i, &keyspaceIDResolverFactoryV3{
-					vindex: colVindex.Vindex,
+					// Only SingleColumn vindexes are returned by FindVindexForSharding.
+					vindex: colVindex.Vindex.(vindexes.SingleColumn),
 				}, nil
 			}
 		}
@@ -158,7 +159,7 @@ func newKeyspaceIDResolverFactoryV3(ctx context.Context, ts *topo.Server, keyspa
 
 // keyspaceIDResolverFactoryV3 uses the Vindex to compute the value.
 type keyspaceIDResolverFactoryV3 struct {
-	vindex vindexes.Vindex
+	vindex vindexes.SingleColumn
 }
 
 func (r *keyspaceIDResolverFactoryV3) keyspaceID(v sqltypes.Value) ([]byte, error) {
diff --git a/go/vt/vtgate/engine/delete.go b/go/vt/vtgate/engine/delete.go
index 9d59c69181..22e2fd61a5 100644
--- a/go/vt/vtgate/engine/delete.go
+++ b/go/vt/vtgate/engine/delete.go
@@ -50,7 +50,7 @@ type Delete struct {
 	Query string
 
 	// Vindex specifies the vindex to be used.
-	Vindex vindexes.Vindex
+	Vindex vindexes.SingleColumn
 	// Values specifies the vindex values to use for routing.
 	// For now, only one value is specified.
 	Values []sqltypes.PlanValue
diff --git a/go/vt/vtgate/engine/insert.go b/go/vt/vtgate/engine/insert.go
index bf1de16def..bf75e6a446 100644
--- a/go/vt/vtgate/engine/insert.go
+++ b/go/vt/vtgate/engine/insert.go
@@ -391,19 +391,10 @@ func (ins *Insert) getInsertShardedRoute(vcursor VCursor, bindVars map[string]*q
 	// keyspace ids. For regular inserts, a failure to find a route
 	// results in an error. For 'ignore' type inserts, the keyspace
 	// id is returned as nil, which is used later to drop the corresponding rows.
-	colVindex := ins.Table.ColumnVindexes[0]
-	keyspaceIDs, err := ins.processPrimary(vcursor, vindexRowsValues[0], colVindex)
+	keyspaceIDs, err := ins.processPrimary(vcursor, vindexRowsValues[0], ins.Table.ColumnVindexes[0])
 	if err != nil {
 		return nil, nil, vterrors.Wrap(err, "getInsertShardedRoute")
 	}
-	// Primary vindex can be owned. If so, go through the processOwned flow.
-	// If not owned, we don't do processUnowned because there's no need to verify
-	// the keyspace ids we just generated.
-	if colVindex.Owned {
-		if err := ins.processOwned(vcursor, vindexRowsValues[0], colVindex, keyspaceIDs); err != nil {
-			return nil, nil, vterrors.Wrap(err, "getInsertShardedRoute")
-		}
-	}
 
 	for vIdx := 1; vIdx < len(ins.Table.ColumnVindexes); vIdx++ {
 		colVindex := ins.Table.ColumnVindexes[vIdx]
diff --git a/go/vt/vtgate/engine/route.go b/go/vt/vtgate/engine/route.go
index 2ad9503882..4576894f13 100644
--- a/go/vt/vtgate/engine/route.go
+++ b/go/vt/vtgate/engine/route.go
@@ -65,7 +65,7 @@ type Route struct {
 	FieldQuery string
 
 	// Vindex specifies the vindex to be used.
-	Vindex vindexes.Vindex
+	Vindex vindexes.SingleColumn
 	// Values specifies the vindex values to use for routing.
 	Values []sqltypes.PlanValue
 
@@ -463,7 +463,7 @@ func (route *Route) sort(in *sqltypes.Result) (*sqltypes.Result, error) {
 	return out, err
 }
 
-func resolveSingleShard(vcursor VCursor, vindex vindexes.Vindex, keyspace *vindexes.Keyspace, vindexKey sqltypes.Value) (*srvtopo.ResolvedShard, []byte, error) {
+func resolveSingleShard(vcursor VCursor, vindex vindexes.SingleColumn, keyspace *vindexes.Keyspace, vindexKey sqltypes.Value) (*srvtopo.ResolvedShard, []byte, error) {
 	destinations, err := vindex.Map(vcursor, []sqltypes.Value{vindexKey})
 	if err != nil {
 		return nil, nil, err
diff --git a/go/vt/vtgate/engine/update.go b/go/vt/vtgate/engine/update.go
index 075a5a8c93..dc3c591546 100644
--- a/go/vt/vtgate/engine/update.go
+++ b/go/vt/vtgate/engine/update.go
@@ -50,7 +50,7 @@ type Update struct {
 	Query string
 
 	// Vindex specifies the vindex to be used.
-	Vindex vindexes.Vindex
+	Vindex vindexes.SingleColumn
 	// Values specifies the vindex values to use for routing.
 	// For now, only one value is specified.
 	Values []sqltypes.PlanValue
diff --git a/go/vt/vtgate/engine/vindex_func.go b/go/vt/vtgate/engine/vindex_func.go
index 99c0a15319..8ffb591405 100644
--- a/go/vt/vtgate/engine/vindex_func.go
+++ b/go/vt/vtgate/engine/vindex_func.go
@@ -35,8 +35,9 @@ type VindexFunc struct {
 	// Fields is the field info for the result.
 	Fields []*querypb.Field
 	// Cols contains source column numbers: 0 for id, 1 for keyspace_id.
-	Cols   []int
-	Vindex vindexes.Vindex
+	Cols []int
+	// TODO(sougou): add support for MultiColumn.
+	Vindex vindexes.SingleColumn
 	Value  sqltypes.PlanValue
 }
 
diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go
index 51440ed530..b3c9e2c898 100644
--- a/go/vt/vtgate/executor.go
+++ b/go/vt/vtgate/executor.go
@@ -1280,7 +1280,11 @@ func (e *Executor) MessageAck(ctx context.Context, keyspace, name string, ids []
 		}
 		// We always use the (unique) primary vindex. The ID must be the
 		// primary vindex for message tables.
-		destinations, err := table.ColumnVindexes[0].Vindex.Map(vcursor, values)
+		single, ok := table.ColumnVindexes[0].Vindex.(vindexes.SingleColumn)
+		if !ok {
+			return 0, fmt.Errorf("multi-column vindexes not supported")
+		}
+		destinations, err := single.Map(vcursor, values)
 		if err != nil {
 			return 0, err
 		}
diff --git a/go/vt/vtgate/planbuilder/from.go b/go/vt/vtgate/planbuilder/from.go
index 7f919ca21c..2c2813bce1 100644
--- a/go/vt/vtgate/planbuilder/from.go
+++ b/go/vt/vtgate/planbuilder/from.go
@@ -195,7 +195,11 @@ func (pb *primitiveBuilder) buildTablePrimitive(tableExpr *sqlparser.AliasedTabl
 		return err
 	}
 	if vindex != nil {
-		pb.bldr, pb.st = newVindexFunc(alias, vindex)
+		single, ok := vindex.(vindexes.SingleColumn)
+		if !ok {
+			return fmt.Errorf("multi-column vindexes not supported")
+		}
+		pb.bldr, pb.st = newVindexFunc(alias, single)
 		return nil
 	}
 
@@ -244,7 +248,8 @@ func (pb *primitiveBuilder) buildTablePrimitive(tableExpr *sqlparser.AliasedTabl
 			// Use the Binary vindex, which is the identity function
 			// for keyspace id.
 			eroute = engine.NewSimpleRoute(engine.SelectEqualUnique, vst.Keyspace)
-			eroute.Vindex, _ = vindexes.NewBinary("binary", nil)
+			vindex, _ = vindexes.NewBinary("binary", nil)
+			eroute.Vindex, _ = vindex.(vindexes.SingleColumn)
 			eroute.Values = []sqltypes.PlanValue{{Value: sqltypes.MakeTrusted(sqltypes.VarBinary, vst.Pinned)}}
 		}
 		// set table name into route
diff --git a/go/vt/vtgate/planbuilder/route_option.go b/go/vt/vtgate/planbuilder/route_option.go
index 5a4f45f60a..1d6ba1c367 100644
--- a/go/vt/vtgate/planbuilder/route_option.go
+++ b/go/vt/vtgate/planbuilder/route_option.go
@@ -37,7 +37,7 @@ type routeOption struct {
 
 	// vindexMap is a map of all vindexMap that can be used
 	// for the routeOption.
-	vindexMap map[*column]vindexes.Vindex
+	vindexMap map[*column]vindexes.SingleColumn
 
 	// condition stores the AST condition that will be used
 	// to resolve the ERoute Values field.
@@ -58,7 +58,7 @@ func newSimpleRouteOption(rb *route, eroute *engine.Route) *routeOption {
 	}
 }
 
-func newRouteOption(rb *route, vst *vindexes.Table, sub *tableSubstitution, vindexMap map[*column]vindexes.Vindex, eroute *engine.Route) *routeOption {
+func newRouteOption(rb *route, vst *vindexes.Table, sub *tableSubstitution, vindexMap map[*column]vindexes.SingleColumn, eroute *engine.Route) *routeOption {
 	var subs []*tableSubstitution
 	if sub != nil && sub.newExpr != nil {
 		subs = []*tableSubstitution{sub}
@@ -95,7 +95,7 @@ func (ro *routeOption) MergeJoin(rro *routeOption, isLeftJoin bool) {
 	// Add RHS vindexes only if it's not a left join.
 	for c, v := range rro.vindexMap {
 		if ro.vindexMap == nil {
-			ro.vindexMap = make(map[*column]vindexes.Vindex)
+			ro.vindexMap = make(map[*column]vindexes.SingleColumn)
 		}
 		ro.vindexMap[c] = v
 	}
@@ -126,7 +126,7 @@ func (ro *routeOption) MergeUnion(rro *routeOption) {
 	ro.substitutions = append(ro.substitutions, rro.substitutions...)
 }
 
-func (ro *routeOption) SubqueryToTable(rb *route, vindexMap map[*column]vindexes.Vindex) {
+func (ro *routeOption) SubqueryToTable(rb *route, vindexMap map[*column]vindexes.SingleColumn) {
 	ro.rb = rb
 	ro.vschemaTable = nil
 	ro.vindexMap = vindexMap
@@ -234,14 +234,14 @@ func (ro *routeOption) UpdatePlan(pb *primitiveBuilder, filter sqlparser.Expr) {
 	}
 }
 
-func (ro *routeOption) updateRoute(opcode engine.RouteOpcode, vindex vindexes.Vindex, condition sqlparser.Expr) {
+func (ro *routeOption) updateRoute(opcode engine.RouteOpcode, vindex vindexes.SingleColumn, condition sqlparser.Expr) {
 	ro.eroute.Opcode = opcode
 	ro.eroute.Vindex = vindex
 	ro.condition = condition
 }
 
 // computePlan computes the plan for the specified filter.
-func (ro *routeOption) computePlan(pb *primitiveBuilder, filter sqlparser.Expr) (opcode engine.RouteOpcode, vindex vindexes.Vindex, condition sqlparser.Expr) {
+func (ro *routeOption) computePlan(pb *primitiveBuilder, filter sqlparser.Expr) (opcode engine.RouteOpcode, vindex vindexes.SingleColumn, condition sqlparser.Expr) {
 	switch node := filter.(type) {
 	case *sqlparser.ComparisonExpr:
 		switch node.Operator {
@@ -257,7 +257,7 @@ func (ro *routeOption) computePlan(pb *primitiveBuilder, filter sqlparser.Expr)
 }
 
 // computeEqualPlan computes the plan for an equality constraint.
-func (ro *routeOption) computeEqualPlan(pb *primitiveBuilder, comparison *sqlparser.ComparisonExpr) (opcode engine.RouteOpcode, vindex vindexes.Vindex, condition sqlparser.Expr) {
+func (ro *routeOption) computeEqualPlan(pb *primitiveBuilder, comparison *sqlparser.ComparisonExpr) (opcode engine.RouteOpcode, vindex vindexes.SingleColumn, condition sqlparser.Expr) {
 	left := comparison.Left
 	right := comparison.Right
 	vindex = ro.FindVindex(pb, left)
@@ -278,7 +278,7 @@ func (ro *routeOption) computeEqualPlan(pb *primitiveBuilder, comparison *sqlpar
 }
 
 // computeINPlan computes the plan for an IN constraint.
-func (ro *routeOption) computeINPlan(pb *primitiveBuilder, comparison *sqlparser.ComparisonExpr) (opcode engine.RouteOpcode, vindex vindexes.Vindex, condition sqlparser.Expr) {
+func (ro *routeOption) computeINPlan(pb *primitiveBuilder, comparison *sqlparser.ComparisonExpr) (opcode engine.RouteOpcode, vindex vindexes.SingleColumn, condition sqlparser.Expr) {
 	vindex = ro.FindVindex(pb, comparison.Left)
 	if vindex == nil {
 		return engine.SelectScatter, nil, nil
@@ -323,7 +323,7 @@ func (ro *routeOption) isBetterThan(other *routeOption) bool {
 	return false
 }
 
-func (ro *routeOption) FindVindex(pb *primitiveBuilder, expr sqlparser.Expr) vindexes.Vindex {
+func (ro *routeOption) FindVindex(pb *primitiveBuilder, expr sqlparser.Expr) vindexes.SingleColumn {
 	col, ok := expr.(*sqlparser.ColName)
 	if !ok {
 		return nil
diff --git a/go/vt/vtgate/planbuilder/symtab.go b/go/vt/vtgate/planbuilder/symtab.go
index fb9ad2301c..654245c318 100644
--- a/go/vt/vtgate/planbuilder/symtab.go
+++ b/go/vt/vtgate/planbuilder/symtab.go
@@ -87,13 +87,13 @@ func newSymtabWithRoute(rb *route) *symtab {
 // AddVSchemaTable takes a list of vschema tables as input and
 // creates a table with multiple route options. It returns a
 // list of vindex maps, one for each input.
-func (st *symtab) AddVSchemaTable(alias sqlparser.TableName, vschemaTables []*vindexes.Table, rb *route) (vindexMaps []map[*column]vindexes.Vindex, err error) {
+func (st *symtab) AddVSchemaTable(alias sqlparser.TableName, vschemaTables []*vindexes.Table, rb *route) (vindexMaps []map[*column]vindexes.SingleColumn, err error) {
 	t := &table{
 		alias:  alias,
 		origin: rb,
 	}
 
-	vindexMaps = make([]map[*column]vindexes.Vindex, len(vschemaTables))
+	vindexMaps = make([]map[*column]vindexes.SingleColumn, len(vschemaTables))
 	for i, vst := range vschemaTables {
 		// The following logic allows the first table to be authoritative while the rest
 		// are not. But there's no need to reveal this flexibility to the user.
@@ -115,8 +115,12 @@ func (st *symtab) AddVSchemaTable(alias sqlparser.TableName, vschemaTables []*vi
 			t.isAuthoritative = true
 		}
 
-		var vindexMap map[*column]vindexes.Vindex
+		var vindexMap map[*column]vindexes.SingleColumn
 		for _, cv := range vst.ColumnVindexes {
+			single, ok := cv.Vindex.(vindexes.SingleColumn)
+			if !ok {
+				continue
+			}
 			for j, cvcol := range cv.Columns {
 				col, err := t.mergeColumn(cvcol, &column{
 					origin: rb,
@@ -128,10 +132,10 @@ func (st *symtab) AddVSchemaTable(alias sqlparser.TableName, vschemaTables []*vi
 				if j == 0 {
 					// For now, only the first column is used for vindex Map functions.
 					if vindexMap == nil {
-						vindexMap = make(map[*column]vindexes.Vindex)
+						vindexMap = make(map[*column]vindexes.SingleColumn)
 					}
-					if vindexMap[col] == nil || vindexMap[col].Cost() > cv.Vindex.Cost() {
-						vindexMap[col] = cv.Vindex
+					if vindexMap[col] == nil || vindexMap[col].Cost() > single.Cost() {
+						vindexMap[col] = single
 					}
 				}
 			}
diff --git a/go/vt/vtgate/planbuilder/update.go b/go/vt/vtgate/planbuilder/update.go
index 78a8f7a14a..f8bd77f793 100644
--- a/go/vt/vtgate/planbuilder/update.go
+++ b/go/vt/vtgate/planbuilder/update.go
@@ -204,7 +204,7 @@ func generateQuery(statement sqlparser.Statement) string {
 
 // getDMLRouting returns the vindex and values for the DML,
 // If it cannot find a unique vindex match, it returns an error.
-func getDMLRouting(where *sqlparser.Where, table *vindexes.Table) (vindexes.Vindex, []sqltypes.PlanValue, error) {
+func getDMLRouting(where *sqlparser.Where, table *vindexes.Table) (vindexes.SingleColumn, []sqltypes.PlanValue, error) {
 	if where == nil {
 		return nil, nil, errors.New("unsupported: multi-shard where clause in DML")
 	}
@@ -212,8 +212,12 @@ func getDMLRouting(where *sqlparser.Where, table *vindexes.Table) (vindexes.Vind
 		if !index.Vindex.IsUnique() {
 			continue
 		}
+		single, ok := index.Vindex.(vindexes.SingleColumn)
+		if !ok {
+			continue
+		}
 		if pv, ok := getMatch(where.Expr, index.Columns[0]); ok {
-			return index.Vindex, []sqltypes.PlanValue{pv}, nil
+			return single, []sqltypes.PlanValue{pv}, nil
 		}
 	}
 	return nil, nil, errors.New("unsupported: multi-shard where clause in DML")
diff --git a/go/vt/vtgate/planbuilder/vindex_func.go b/go/vt/vtgate/planbuilder/vindex_func.go
index 6135338c12..3902f4a103 100644
--- a/go/vt/vtgate/planbuilder/vindex_func.go
+++ b/go/vt/vtgate/planbuilder/vindex_func.go
@@ -40,7 +40,7 @@ type vindexFunc struct {
 	eVindexFunc *engine.VindexFunc
 }
 
-func newVindexFunc(alias sqlparser.TableName, vindex vindexes.Vindex) (*vindexFunc, *symtab) {
+func newVindexFunc(alias sqlparser.TableName, vindex vindexes.SingleColumn) (*vindexFunc, *symtab) {
 	vf := &vindexFunc{
 		order: 1,
 		eVindexFunc: &engine.VindexFunc{
diff --git a/go/vt/vtgate/vindexes/binary.go b/go/vt/vtgate/vindexes/binary.go
index 1d9e2bf132..50d0500a9b 100644
--- a/go/vt/vtgate/vindexes/binary.go
+++ b/go/vt/vtgate/vindexes/binary.go
@@ -25,8 +25,8 @@ import (
 )
 
 var (
-	_ Vindex     = (*Binary)(nil)
-	_ Reversible = (*Binary)(nil)
+	_ SingleColumn = (*Binary)(nil)
+	_ Reversible   = (*Binary)(nil)
 )
 
 // Binary is a vindex that converts binary bits to a keyspace id.
diff --git a/go/vt/vtgate/vindexes/binarymd5.go b/go/vt/vtgate/vindexes/binarymd5.go
index a75326e276..be6cea7931 100644
--- a/go/vt/vtgate/vindexes/binarymd5.go
+++ b/go/vt/vtgate/vindexes/binarymd5.go
@@ -25,7 +25,7 @@ import (
 )
 
 var (
-	_ Vindex = (*BinaryMD5)(nil)
+	_ SingleColumn = (*BinaryMD5)(nil)
 )
 
 // BinaryMD5 is a vindex that hashes binary bits to a keyspace id.
diff --git a/go/vt/vtgate/vindexes/consistent_lookup.go b/go/vt/vtgate/vindexes/consistent_lookup.go
index 546b70c244..6d862e7c8c 100644
--- a/go/vt/vtgate/vindexes/consistent_lookup.go
+++ b/go/vt/vtgate/vindexes/consistent_lookup.go
@@ -33,10 +33,10 @@ import (
 )
 
 var (
-	_ Vindex        = (*ConsistentLookupUnique)(nil)
+	_ SingleColumn  = (*ConsistentLookupUnique)(nil)
 	_ Lookup        = (*ConsistentLookupUnique)(nil)
 	_ WantOwnerInfo = (*ConsistentLookupUnique)(nil)
-	_ Vindex        = (*ConsistentLookup)(nil)
+	_ SingleColumn  = (*ConsistentLookup)(nil)
 	_ Lookup        = (*ConsistentLookup)(nil)
 	_ WantOwnerInfo = (*ConsistentLookup)(nil)
 )
diff --git a/go/vt/vtgate/vindexes/hash.go b/go/vt/vtgate/vindexes/hash.go
index 43859e4f38..db7b86e638 100644
--- a/go/vt/vtgate/vindexes/hash.go
+++ b/go/vt/vtgate/vindexes/hash.go
@@ -31,8 +31,8 @@ import (
 )
 
 var (
-	_ Vindex     = (*Hash)(nil)
-	_ Reversible = (*Hash)(nil)
+	_ SingleColumn = (*Hash)(nil)
+	_ Reversible   = (*Hash)(nil)
 )
 
 // Hash defines vindex that hashes an int64 to a KeyspaceId
diff --git a/go/vt/vtgate/vindexes/lookup.go b/go/vt/vtgate/vindexes/lookup.go
index a6869066e4..6f13abb0e5 100644
--- a/go/vt/vtgate/vindexes/lookup.go
+++ b/go/vt/vtgate/vindexes/lookup.go
@@ -27,10 +27,10 @@ import (
 )
 
 var (
-	_ Vindex = (*LookupUnique)(nil)
-	_ Lookup = (*LookupUnique)(nil)
-	_ Vindex = (*LookupNonUnique)(nil)
-	_ Lookup = (*LookupNonUnique)(nil)
+	_ SingleColumn = (*LookupUnique)(nil)
+	_ Lookup       = (*LookupUnique)(nil)
+	_ SingleColumn = (*LookupNonUnique)(nil)
+	_ Lookup       = (*LookupNonUnique)(nil)
 )
 
 func init() {
diff --git a/go/vt/vtgate/vindexes/lookup_hash.go b/go/vt/vtgate/vindexes/lookup_hash.go
index 30118bd274..46809ad4b7 100644
--- a/go/vt/vtgate/vindexes/lookup_hash.go
+++ b/go/vt/vtgate/vindexes/lookup_hash.go
@@ -27,10 +27,10 @@ import (
 )
 
 var (
-	_ Vindex = (*LookupHash)(nil)
-	_ Lookup = (*LookupHash)(nil)
-	_ Vindex = (*LookupHashUnique)(nil)
-	_ Lookup = (*LookupHashUnique)(nil)
+	_ SingleColumn = (*LookupHash)(nil)
+	_ Lookup       = (*LookupHash)(nil)
+	_ SingleColumn = (*LookupHashUnique)(nil)
+	_ Lookup       = (*LookupHashUnique)(nil)
 )
 
 func init() {
diff --git a/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go b/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
index 30b12a33f3..f56d302c81 100644
--- a/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
+++ b/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
@@ -28,10 +28,10 @@ import (
 )
 
 var (
-	_ Vindex = (*LookupUnicodeLooseMD5Hash)(nil)
-	_ Lookup = (*LookupUnicodeLooseMD5Hash)(nil)
-	_ Vindex = (*LookupUnicodeLooseMD5HashUnique)(nil)
-	_ Lookup = (*LookupUnicodeLooseMD5HashUnique)(nil)
+	_ SingleColumn = (*LookupUnicodeLooseMD5Hash)(nil)
+	_ Lookup       = (*LookupUnicodeLooseMD5Hash)(nil)
+	_ SingleColumn = (*LookupUnicodeLooseMD5HashUnique)(nil)
+	_ Lookup       = (*LookupUnicodeLooseMD5HashUnique)(nil)
 )
 
 func init() {
diff --git a/go/vt/vtgate/vindexes/numeric.go b/go/vt/vtgate/vindexes/numeric.go
index 1b0b5c2a07..8ebdbe5c2d 100644
--- a/go/vt/vtgate/vindexes/numeric.go
+++ b/go/vt/vtgate/vindexes/numeric.go
@@ -27,8 +27,8 @@ import (
 )
 
 var (
-	_ Vindex     = (*Numeric)(nil)
-	_ Reversible = (*Numeric)(nil)
+	_ SingleColumn = (*Numeric)(nil)
+	_ Reversible   = (*Numeric)(nil)
 )
 
 // Numeric defines a bit-pattern mapping of a uint64 to the KeyspaceId.
diff --git a/go/vt/vtgate/vindexes/numeric_static_map.go b/go/vt/vtgate/vindexes/numeric_static_map.go
index 9f51a80a88..39832aa377 100644
--- a/go/vt/vtgate/vindexes/numeric_static_map.go
+++ b/go/vt/vtgate/vindexes/numeric_static_map.go
@@ -30,7 +30,7 @@ import (
 )
 
 var (
-	_ Vindex = (*NumericStaticMap)(nil)
+	_ SingleColumn = (*NumericStaticMap)(nil)
 )
 
 // NumericLookupTable stores the mapping of keys.
diff --git a/go/vt/vtgate/vindexes/region_experimental.go b/go/vt/vtgate/vindexes/region_experimental.go
index 15159517dd..ba10a10f7d 100644
--- a/go/vt/vtgate/vindexes/region_experimental.go
+++ b/go/vt/vtgate/vindexes/region_experimental.go
@@ -26,10 +26,7 @@ import (
 )
 
 var (
-	_ Vindex        = (*RegionExperimental)(nil)
-	_ Lookup        = (*RegionExperimental)(nil)
-	_ WantOwnerInfo = (*RegionExperimental)(nil)
-	_ MultiColumn   = (*RegionExperimental)(nil)
+	_ MultiColumn = (*RegionExperimental)(nil)
 )
 
 func init() {
@@ -40,8 +37,8 @@ func init() {
 // The table is expected to define the id column as unique. It's
 // Unique and a Lookup.
 type RegionExperimental struct {
+	name        string
 	regionBytes int
-	*ConsistentLookupUnique
 }
 
 // NewRegionExperimental creates a RegionExperimental vindex.
@@ -61,45 +58,51 @@ func NewRegionExperimental(name string, m map[string]string) (Vindex, error) {
 	default:
 		return nil, fmt.Errorf("region_bits must be 1 or 2: %v", rbs)
 	}
-	vindex, err := NewConsistentLookupUnique(name, m)
-	if err != nil {
-		// Unreachable.
-		return nil, err
-	}
-	cl := vindex.(*ConsistentLookupUnique)
-	if len(cl.lkp.FromColumns) != 2 {
-		return nil, fmt.Errorf("two columns are required for region_experimental: %v", cl.lkp.FromColumns)
-	}
 	return &RegionExperimental{
-		regionBytes:            rb,
-		ConsistentLookupUnique: cl,
+		name:        name,
+		regionBytes: rb,
 	}, nil
 }
 
-// MapMulti satisfies MultiColumn.
-func (ge *RegionExperimental) MapMulti(vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error) {
+// String returns the name of the vindex.
+func (ge *RegionExperimental) String() string {
+	return ge.name
+}
+
+// Cost returns the cost of this index as 1.
+func (ge *RegionExperimental) Cost() int {
+	return 1
+}
+
+// IsUnique returns true since the Vindex is unique.
+func (ge *RegionExperimental) IsUnique() bool {
+	return true
+}
+
+// Map satisfies MultiColumn.
+func (ge *RegionExperimental) Map(vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error) {
 	destinations := make([]key.Destination, 0, len(rowsColValues))
 	for _, row := range rowsColValues {
 		if len(row) != 2 {
 			destinations = append(destinations, key.DestinationNone{})
 			continue
 		}
-		// Compute hash.
-		hn, err := sqltypes.ToUint64(row[0])
+		// Compute region prefix.
+		rn, err := sqltypes.ToUint64(row[0])
 		if err != nil {
 			destinations = append(destinations, key.DestinationNone{})
 			continue
 		}
-		h := vhash(hn)
+		r := make([]byte, 2, 2+8)
+		binary.BigEndian.PutUint16(r, uint16(rn))
 
-		// Compute region prefix.
-		rn, err := sqltypes.ToUint64(row[1])
+		// Compute hash.
+		hn, err := sqltypes.ToUint64(row[1])
 		if err != nil {
 			destinations = append(destinations, key.DestinationNone{})
 			continue
 		}
-		r := make([]byte, 2)
-		binary.BigEndian.PutUint16(r, uint16(rn))
+		h := vhash(hn)
 
 		// Concatenate and add to destinations.
 		if ge.regionBytes == 1 {
@@ -111,10 +114,10 @@ func (ge *RegionExperimental) MapMulti(vcursor VCursor, rowsColValues [][]sqltyp
 	return destinations, nil
 }
 
-// VerifyMulti satisfies MultiColumn.
-func (ge *RegionExperimental) VerifyMulti(vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte) ([]bool, error) {
+// Verify satisfies MultiColumn.
+func (ge *RegionExperimental) Verify(vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte) ([]bool, error) {
 	result := make([]bool, len(rowsColValues))
-	destinations, _ := ge.MapMulti(vcursor, rowsColValues)
+	destinations, _ := ge.Map(vcursor, rowsColValues)
 	for i, dest := range destinations {
 		destksid, ok := dest.(key.DestinationKeyspaceID)
 		if !ok {
@@ -122,14 +125,5 @@ func (ge *RegionExperimental) VerifyMulti(vcursor VCursor, rowsColValues [][]sql
 		}
 		result[i] = bytes.Equal([]byte(destksid), ksids[i])
 	}
-	// We also need to verify from the lookup.
-	// TODO(sougou): we should only verify true values from previous result.
-	lresult, err := Verify(ge.ConsistentLookupUnique, vcursor, rowsColValues, ksids)
-	if err != nil {
-		return nil, err
-	}
-	for i := range result {
-		result[i] = result[i] && lresult[i]
-	}
 	return result, nil
 }
diff --git a/go/vt/vtgate/vindexes/reverse_bits.go b/go/vt/vtgate/vindexes/reverse_bits.go
index 9957988fe5..f199eaaea3 100644
--- a/go/vt/vtgate/vindexes/reverse_bits.go
+++ b/go/vt/vtgate/vindexes/reverse_bits.go
@@ -29,8 +29,8 @@ import (
 )
 
 var (
-	_ Vindex     = (*ReverseBits)(nil)
-	_ Reversible = (*ReverseBits)(nil)
+	_ SingleColumn = (*ReverseBits)(nil)
+	_ Reversible   = (*ReverseBits)(nil)
 )
 
 // ReverseBits defines vindex that reverses the bits of a number.
diff --git a/go/vt/vtgate/vindexes/unicodeloosemd5.go b/go/vt/vtgate/vindexes/unicodeloosemd5.go
index 8264ebfe91..ef3a70c9f1 100644
--- a/go/vt/vtgate/vindexes/unicodeloosemd5.go
+++ b/go/vt/vtgate/vindexes/unicodeloosemd5.go
@@ -30,7 +30,7 @@ import (
 )
 
 var (
-	_ Vindex = (*UnicodeLooseMD5)(nil)
+	_ SingleColumn = (*UnicodeLooseMD5)(nil)
 )
 
 // UnicodeLooseMD5 is a vindex that normalizes and hashes unicode strings
diff --git a/go/vt/vtgate/vindexes/vindex.go b/go/vt/vtgate/vindexes/vindex.go
index 35ab8c853e..3d06668def 100644
--- a/go/vt/vtgate/vindexes/vindex.go
+++ b/go/vt/vtgate/vindexes/vindex.go
@@ -22,9 +22,11 @@ import (
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 	"vitess.io/vitess/go/vt/sqlparser"
+	"vitess.io/vitess/go/vt/vterrors"
 
 	querypb "vitess.io/vitess/go/vt/proto/query"
 	vtgatepb "vitess.io/vitess/go/vt/proto/vtgate"
+	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
 )
 
 // This file defines interfaces and registration for vindexes.
@@ -56,33 +58,37 @@ type Vindex interface {
 	// IsUnique returns true if the Vindex is unique.
 	// Which means Map() maps to either a KeyRange or a single KeyspaceID.
 	IsUnique() bool
+}
 
+// SingleColumn defines the interface for a single column vindex.
+type SingleColumn interface {
+	Vindex
 	// Map can map ids to key.Destination objects.
 	// If the Vindex is unique, each id would map to either
 	// a KeyRange, or a single KeyspaceID.
 	// If the Vindex is non-unique, each id would map to either
 	// a KeyRange, or a list of KeyspaceID.
-	// If the error returned if nil, then the array len of the
-	// key.Destination array must match len(ids).
 	Map(vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error)
 
-	// Verify must be implented by all vindexes. It should return
-	// true if the ids can be mapped to the keyspace ids.
+	// Verify returns true for every id that successfully maps to the
+	// specified keyspace id.
 	Verify(vcursor VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error)
 }
 
-// MultiColumn defines the interface for vindexes that can
-// support multi-column vindexes.
+// MultiColumn defines the interface for a multi-column vindex.
 type MultiColumn interface {
-	MapMulti(vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error)
-	VerifyMulti(vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte) ([]bool, error)
+	Vindex
+	Map(vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error)
+	Verify(vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte) ([]bool, error)
 }
 
 // A Reversible vindex is one that can perform a
 // reverse lookup from a keyspace id to an id. This
 // is optional. If present, VTGate can use it to
 // fill column values based on the target keyspace id.
+// Reversible is supported only for SingleColumn vindexes.
 type Reversible interface {
+	SingleColumn
 	ReverseMap(vcursor VCursor, ks [][]byte) ([]sqltypes.Value, error)
 }
 
@@ -140,20 +146,26 @@ func CreateVindex(vindexType, name string, params map[string]string) (Vindex, er
 	return f(name, params)
 }
 
-// Map invokes MapMulti or Map depending on which is available.
+// Map invokes the Map implementation supplied by the vindex.
 func Map(vindex Vindex, vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error) {
-	if multi, ok := vindex.(MultiColumn); ok {
-		return multi.MapMulti(vcursor, rowsColValues)
+	switch vindex := vindex.(type) {
+	case MultiColumn:
+		return vindex.Map(vcursor, rowsColValues)
+	case SingleColumn:
+		return vindex.Map(vcursor, firstColsOnly(rowsColValues))
 	}
-	return vindex.Map(vcursor, firstColsOnly(rowsColValues))
+	return nil, vterrors.New(vtrpcpb.Code_INTERNAL, "vindex does not have Map functions")
 }
 
-// Verify invokes VerifyMulti or Verify depending on which is available.
+// Verify invokes the Verify implementation supplied by the vindex.
 func Verify(vindex Vindex, vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte) ([]bool, error) {
-	if multi, ok := vindex.(MultiColumn); ok {
-		return multi.VerifyMulti(vcursor, rowsColValues, ksids)
+	switch vindex := vindex.(type) {
+	case MultiColumn:
+		return vindex.Verify(vcursor, rowsColValues, ksids)
+	case SingleColumn:
+		return vindex.Verify(vcursor, firstColsOnly(rowsColValues), ksids)
 	}
-	return vindex.Verify(vcursor, firstColsOnly(rowsColValues), ksids)
+	return nil, vterrors.New(vtrpcpb.Code_INTERNAL, "vindex does not have Map functions")
 }
 
 func firstColsOnly(rowsColValues [][]sqltypes.Value) []sqltypes.Value {
diff --git a/go/vt/vtgate/vindexes/vschema.go b/go/vt/vtgate/vindexes/vschema.go
index 8f519fb709..a1f15ad862 100644
--- a/go/vt/vtgate/vindexes/vschema.go
+++ b/go/vt/vtgate/vindexes/vschema.go
@@ -313,6 +313,9 @@ func buildTables(ks *vschemapb.Keyspace, vschema *VSchema, ksvschema *KeyspaceSc
 				if !columnVindex.Vindex.IsUnique() {
 					return fmt.Errorf("primary vindex %s is not Unique for table %s", ind.Name, tname)
 				}
+				if owned {
+					return fmt.Errorf("primary vindex %s cannot be owned for table %s", ind.Name, tname)
+				}
 			}
 			t.ColumnVindexes = append(t.ColumnVindexes, columnVindex)
 			if owned {
@@ -617,6 +620,10 @@ func FindVindexForSharding(tableName string, colVindexes []*ColumnVindex) (*Colu
 	}
 	result := colVindexes[0]
 	for _, colVindex := range colVindexes {
+		// Only allow SingleColumn for legacy resharding.
+		if _, ok := colVindex.Vindex.(SingleColumn); !ok {
+			continue
+		}
 		if colVindex.Vindex.Cost() < result.Vindex.Cost() && colVindex.Vindex.IsUnique() {
 			result = colVindex
 		}
diff --git a/go/vt/vtgate/vindexes/xxhash.go b/go/vt/vtgate/vindexes/xxhash.go
index 48677a736f..fd648e3117 100644
--- a/go/vt/vtgate/vindexes/xxhash.go
+++ b/go/vt/vtgate/vindexes/xxhash.go
@@ -27,7 +27,7 @@ import (
 )
 
 var (
-	_ Vindex = (*XXHash)(nil)
+	_ SingleColumn = (*XXHash)(nil)
 )
 
 // XXHash defines vindex that hashes any sql types to a KeyspaceId
diff --git a/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go b/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go
index e13b466b55..8e8f211cde 100644
--- a/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go
+++ b/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go
@@ -35,19 +35,28 @@ import (
 
 // Plan represents the plan for a table.
 type Plan struct {
-	Table        *Table
-	ColExprs     []ColExpr
-	VindexColumn int
-	Vindex       vindexes.Vindex
-	KeyRange     *topodatapb.KeyRange
+	Table    *Table
+	ColExprs []ColExpr
+
+	// Vindex, VindexColumns and KeyRange, if set, will be used
+	// to filter the row.
+	Vindex        vindexes.Vindex
+	VindexColumns []int
+	KeyRange      *topodatapb.KeyRange
 }
 
 // ColExpr represents a column expression.
 type ColExpr struct {
+	// ColNum specifies the source column value.
 	ColNum int
-	Vindex vindexes.Vindex
-	Alias  sqlparser.ColIdent
-	Type   querypb.Type
+
+	// Vindex and VindexColumns, if set, will be used to generate
+	// a keyspace_id. If so, ColNum is ignored.
+	Vindex        vindexes.Vindex
+	VindexColumns []int
+
+	Alias sqlparser.ColIdent
+	Type  querypb.Type
 }
 
 // Table contains the metadata for a table.
@@ -71,37 +80,44 @@ func (plan *Plan) fields() []*querypb.Field {
 // filter filters the row against the plan. It returns false if the row did not match.
 // If the row matched, it returns the columns to be sent.
 func (plan *Plan) filter(values []sqltypes.Value) (bool, []sqltypes.Value, error) {
+	if plan.Vindex != nil {
+		vindexValues := make([]sqltypes.Value, 0, len(plan.VindexColumns))
+		for _, col := range plan.VindexColumns {
+			vindexValues = append(vindexValues, values[col])
+		}
+		ksid, err := getKeyspaceID(vindexValues, plan.Vindex)
+		if err != nil {
+			return false, nil, err
+		}
+		if !key.KeyRangeContains(plan.KeyRange, ksid) {
+			return false, nil, nil
+		}
+	}
+
 	result := make([]sqltypes.Value, len(plan.ColExprs))
 	for i, colExpr := range plan.ColExprs {
 		if colExpr.ColNum >= len(values) {
 			return false, nil, fmt.Errorf("index out of range, colExpr.ColNum: %d, len(values): %d", colExpr.ColNum, len(values))
 		}
-		val := values[colExpr.ColNum]
-		if colExpr.Vindex != nil {
-			ksid, err := getKeyspaceID(val, colExpr.Vindex)
+		if colExpr.Vindex == nil {
+			result[i] = values[colExpr.ColNum]
+		} else {
+			vindexValues := make([]sqltypes.Value, 0, len(colExpr.VindexColumns))
+			for _, col := range colExpr.VindexColumns {
+				vindexValues = append(vindexValues, values[col])
+			}
+			ksid, err := getKeyspaceID(vindexValues, colExpr.Vindex)
 			if err != nil {
 				return false, nil, err
 			}
-			val = sqltypes.MakeTrusted(sqltypes.VarBinary, []byte(ksid))
+			result[i] = sqltypes.MakeTrusted(sqltypes.VarBinary, []byte(ksid))
 		}
-		result[i] = val
-	}
-	if plan.Vindex == nil {
-		return true, result, nil
-	}
-
-	ksid, err := getKeyspaceID(result[plan.VindexColumn], plan.Vindex)
-	if err != nil {
-		return false, nil, err
-	}
-	if !key.KeyRangeContains(plan.KeyRange, ksid) {
-		return false, nil, nil
 	}
 	return true, result, nil
 }
 
-func getKeyspaceID(value sqltypes.Value, vindex vindexes.Vindex) (key.DestinationKeyspaceID, error) {
-	destinations, err := vindex.Map(nil, []sqltypes.Value{value})
+func getKeyspaceID(values []sqltypes.Value, vindex vindexes.Vindex) (key.DestinationKeyspaceID, error) {
+	destinations, err := vindexes.Map(vindex, nil, [][]sqltypes.Value{values})
 	if err != nil {
 		return nil, err
 	}
@@ -110,7 +126,7 @@ func getKeyspaceID(value sqltypes.Value, vindex vindexes.Vindex) (key.Destinatio
 	}
 	ksid, ok := destinations[0].(key.DestinationKeyspaceID)
 	if !ok || len(ksid) == 0 {
-		return nil, fmt.Errorf("could not map %v to a keyspace id, got destination %v", value, destinations[0])
+		return nil, fmt.Errorf("could not map %v to a keyspace id, got destination %v", values, destinations[0])
 	}
 	return ksid, nil
 }
@@ -223,14 +239,12 @@ func buildREPlan(ti *Table, kschema *vindexes.KeyspaceSchema, filter string) (*P
 	if len(table.ColumnVindexes) == 0 {
 		return nil, fmt.Errorf("table %s has no primary vindex", ti.Name)
 	}
-	// findColumn can be used here because result column list is same
-	// as source.
-	colnum, err := findColumn(ti, table.ColumnVindexes[0].Columns[0])
+	plan.Vindex = table.ColumnVindexes[0].Vindex
+	var err error
+	plan.VindexColumns, err = buildVindexColumns(plan.Table, table.ColumnVindexes[0].Columns)
 	if err != nil {
 		return nil, err
 	}
-	plan.VindexColumn = colnum
-	plan.Vindex = table.ColumnVindexes[0].Vindex
 
 	// Parse keyrange.
 	keyranges, err := key.ParseShardingSpec(filter)
@@ -341,7 +355,11 @@ func (plan *Plan) analyzeExpr(kschema *vindexes.KeyspaceSchema, selExpr sqlparse
 		if as.IsEmpty() {
 			as = sqlparser.NewColIdent(sqlparser.String(aliased.Expr))
 		}
-		return ColExpr{ColNum: colnum, Alias: as, Type: plan.Table.Columns[colnum].Type}, nil
+		return ColExpr{
+			ColNum: colnum,
+			Alias:  as,
+			Type:   plan.Table.Columns[colnum].Type,
+		}, nil
 	case *sqlparser.FuncExpr:
 		if inner.Name.Lowered() != "keyspace_id" {
 			return ColExpr{}, fmt.Errorf("unsupported function: %v", sqlparser.String(inner))
@@ -357,21 +375,26 @@ func (plan *Plan) analyzeExpr(kschema *vindexes.KeyspaceSchema, selExpr sqlparse
 		if len(table.ColumnVindexes) == 0 {
 			return ColExpr{}, fmt.Errorf("table %s has no primary vindex", plan.Table.Name)
 		}
-		colnum, err := findColumn(plan.Table, table.ColumnVindexes[0].Columns[0])
+		vindexColumns, err := buildVindexColumns(plan.Table, table.ColumnVindexes[0].Columns)
 		if err != nil {
 			return ColExpr{}, err
 		}
-		return ColExpr{ColNum: colnum, Vindex: table.ColumnVindexes[0].Vindex, Alias: sqlparser.NewColIdent("keyspace_id"), Type: sqltypes.VarBinary}, nil
+		return ColExpr{
+			Vindex:        table.ColumnVindexes[0].Vindex,
+			VindexColumns: vindexColumns,
+			Alias:         sqlparser.NewColIdent("keyspace_id"),
+			Type:          sqltypes.VarBinary,
+		}, nil
 	default:
 		return ColExpr{}, fmt.Errorf("unsupported: %v", sqlparser.String(aliased.Expr))
 	}
 }
 
 func (plan *Plan) analyzeInKeyRange(kschema *vindexes.KeyspaceSchema, exprs sqlparser.SelectExprs) error {
-	var colname sqlparser.ColIdent
+	var colnames []sqlparser.ColIdent
 	var krExpr sqlparser.SelectExpr
-	switch len(exprs) {
-	case 1:
+	switch {
+	case len(exprs) == 1:
 		table := kschema.Tables[plan.Table.Name]
 		if table == nil {
 			return fmt.Errorf("no vschema definition for table %s", plan.Table.Name)
@@ -380,23 +403,26 @@ func (plan *Plan) analyzeInKeyRange(kschema *vindexes.KeyspaceSchema, exprs sqlp
 		if len(table.ColumnVindexes) == 0 {
 			return fmt.Errorf("table %s has no primary vindex", plan.Table.Name)
 		}
-		colname = table.ColumnVindexes[0].Columns[0]
+		colnames = table.ColumnVindexes[0].Columns
 		plan.Vindex = table.ColumnVindexes[0].Vindex
 		krExpr = exprs[0]
-	case 3:
-		aexpr, ok := exprs[0].(*sqlparser.AliasedExpr)
-		if !ok {
-			return fmt.Errorf("unexpected: %v", sqlparser.String(exprs[0]))
-		}
-		qualifiedName, ok := aexpr.Expr.(*sqlparser.ColName)
-		if !ok {
-			return fmt.Errorf("unexpected: %v", sqlparser.String(exprs[0]))
-		}
-		if !qualifiedName.Qualifier.IsEmpty() {
-			return fmt.Errorf("unsupported qualifier for column: %v", sqlparser.String(colname))
+	case len(exprs) >= 3:
+		for _, expr := range exprs[:len(exprs)-2] {
+			aexpr, ok := expr.(*sqlparser.AliasedExpr)
+			if !ok {
+				return fmt.Errorf("unexpected: %v", sqlparser.String(expr))
+			}
+			qualifiedName, ok := aexpr.Expr.(*sqlparser.ColName)
+			if !ok {
+				return fmt.Errorf("unexpected: %v", sqlparser.String(expr))
+			}
+			if !qualifiedName.Qualifier.IsEmpty() {
+				return fmt.Errorf("unsupported qualifier for column: %v", sqlparser.String(qualifiedName))
+			}
+			colnames = append(colnames, qualifiedName.Name)
 		}
-		colname = qualifiedName.Name
-		vtype, err := selString(exprs[1])
+
+		vtype, err := selString(exprs[len(exprs)-2])
 		if err != nil {
 			return err
 		}
@@ -407,20 +433,15 @@ func (plan *Plan) analyzeInKeyRange(kschema *vindexes.KeyspaceSchema, exprs sqlp
 		if !plan.Vindex.IsUnique() {
 			return fmt.Errorf("vindex must be Unique to be used for VReplication: %s", vtype)
 		}
-		krExpr = exprs[2]
+
+		krExpr = exprs[len(exprs)-1]
 	default:
 		return fmt.Errorf("unexpected in_keyrange parameters: %v", sqlparser.String(exprs))
 	}
-	found := false
-	for i, cExpr := range plan.ColExprs {
-		if cExpr.Alias.Equal(colname) {
-			found = true
-			plan.VindexColumn = i
-			break
-		}
-	}
-	if !found {
-		return fmt.Errorf("keyrange expression does not reference a column in the select list: %v", sqlparser.String(colname))
+	var err error
+	plan.VindexColumns, err = buildVindexColumns(plan.Table, colnames)
+	if err != nil {
+		return err
 	}
 	kr, err := selString(krExpr)
 	if err != nil {
@@ -449,6 +470,18 @@ func selString(expr sqlparser.SelectExpr) (string, error) {
 	return string(val.Val), nil
 }
 
+func buildVindexColumns(ti *Table, colnames []sqlparser.ColIdent) ([]int, error) {
+	vindexColumns := make([]int, 0, len(colnames))
+	for _, colname := range colnames {
+		colnum, err := findColumn(ti, colname)
+		if err != nil {
+			return nil, err
+		}
+		vindexColumns = append(vindexColumns, colnum)
+	}
+	return vindexColumns, nil
+}
+
 func findColumn(ti *Table, name sqlparser.ColIdent) (int, error) {
 	for i, col := range ti.Columns {
 		if name.Equal(col.Name) {
diff --git a/go/vt/worker/key_resolver.go b/go/vt/worker/key_resolver.go
index bc53eafd1c..a79fb1b66f 100644
--- a/go/vt/worker/key_resolver.go
+++ b/go/vt/worker/key_resolver.go
@@ -93,7 +93,7 @@ func (r *v2Resolver) keyspaceID(row []sqltypes.Value) ([]byte, error) {
 // table.
 type v3Resolver struct {
 	shardingColumnIndex int
-	vindex              vindexes.Vindex
+	vindex              vindexes.SingleColumn
 }
 
 // newV3ResolverFromTableDefinition returns a keyspaceIDResolver for a v3 table.
@@ -119,7 +119,8 @@ func newV3ResolverFromTableDefinition(keyspaceSchema *vindexes.KeyspaceSchema, t
 
 	return &v3Resolver{
 		shardingColumnIndex: columnIndex,
-		vindex:              colVindex.Vindex,
+		// Only SingleColumn vindexes are returned by FindVindexForSharding.
+		vindex: colVindex.Vindex.(vindexes.SingleColumn),
 	}, nil
 }
 
@@ -149,7 +150,8 @@ func newV3ResolverFromColumnList(keyspaceSchema *vindexes.KeyspaceSchema, name s
 
 	return &v3Resolver{
 		shardingColumnIndex: columnIndex,
-		vindex:              colVindex.Vindex,
+		// Only SingleColumn vindexes are returned by FindVindexForSharding.
+		vindex: colVindex.Vindex.(vindexes.SingleColumn),
 	}, nil
 }
 


Test patch:
diff --git a/go/vt/vtgate/engine/delete_test.go b/go/vt/vtgate/engine/delete_test.go
index a61dcc702e..d2b7c9173a 100644
--- a/go/vt/vtgate/engine/delete_test.go
+++ b/go/vt/vtgate/engine/delete_test.go
@@ -65,7 +65,7 @@ func TestDeleteEqual(t *testing.T) {
 			Sharded: true,
 		},
 		Query:  "dummy_delete",
-		Vindex: vindex,
+		Vindex: vindex.(vindexes.SingleColumn),
 		Values: []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}},
 	}
 
@@ -98,7 +98,7 @@ func TestDeleteEqualNoRoute(t *testing.T) {
 			Sharded: true,
 		},
 		Query:  "dummy_delete",
-		Vindex: vindex,
+		Vindex: vindex.(vindexes.SingleColumn),
 		Values: []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}},
 	}
 
@@ -127,7 +127,7 @@ func TestDeleteEqualNoScatter(t *testing.T) {
 			Sharded: true,
 		},
 		Query:  "dummy_delete",
-		Vindex: vindex,
+		Vindex: vindex.(vindexes.SingleColumn),
 		Values: []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}},
 	}
 
@@ -142,7 +142,7 @@ func TestDeleteOwnedVindex(t *testing.T) {
 		Opcode:           DeleteEqual,
 		Keyspace:         ks.Keyspace,
 		Query:            "dummy_delete",
-		Vindex:           ks.Vindexes["hash"],
+		Vindex:           ks.Vindexes["hash"].(vindexes.SingleColumn),
 		Values:           []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}},
 		Table:            ks.Tables["t1"],
 		OwnedVindexQuery: "dummy_subquery",
diff --git a/go/vt/vtgate/engine/insert_test.go b/go/vt/vtgate/engine/insert_test.go
index d8373863f9..7989ae64f3 100644
--- a/go/vt/vtgate/engine/insert_test.go
+++ b/go/vt/vtgate/engine/insert_test.go
@@ -20,7 +20,6 @@ import (
 	"errors"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/vtgate/vindexes"
 
@@ -655,9 +654,14 @@ func TestInsertShardedGeo(t *testing.T) {
 						Type: "region_experimental",
 						Params: map[string]string{
 							"region_bytes": "1",
-							"table":        "lkp",
-							"from":         "id,region",
-							"to":           "toc",
+						},
+					},
+					"lookup": {
+						Type: "lookup_unique",
+						Params: map[string]string{
+							"table": "id_idx",
+							"from":  "id",
+							"to":    "keyspace_id",
 						},
 						Owner: "t1",
 					},
@@ -666,7 +670,10 @@ func TestInsertShardedGeo(t *testing.T) {
 					"t1": {
 						ColumnVindexes: []*vschemapb.ColumnVindex{{
 							Name:    "geo",
-							Columns: []string{"id", "region"},
+							Columns: []string{"region", "id"},
+						}, {
+							Name:    "lookup",
+							Columns: []string{"id"},
 						}},
 					},
 				},
@@ -683,20 +690,30 @@ func TestInsertShardedGeo(t *testing.T) {
 		InsertSharded,
 		ks.Keyspace,
 		[]sqltypes.PlanValue{{
-			// colVindex columns: id, region
+			// colVindex columns: region, id
 			Values: []sqltypes.PlanValue{{
+				// rows for region
+				Values: []sqltypes.PlanValue{{
+					Value: sqltypes.NewInt64(1),
+				}, {
+					Value: sqltypes.NewInt64(255),
+				}},
+			}, {
 				// rows for id
 				Values: []sqltypes.PlanValue{{
 					Value: sqltypes.NewInt64(1),
 				}, {
 					Value: sqltypes.NewInt64(1),
 				}},
-			}, {
-				// rows for region
+			}},
+		}, {
+			// colVindex columns: id
+			Values: []sqltypes.PlanValue{{
+				// rows for id
 				Values: []sqltypes.PlanValue{{
 					Value: sqltypes.NewInt64(1),
 				}, {
-					Value: sqltypes.NewInt64(255),
+					Value: sqltypes.NewInt64(1),
 				}},
 			}},
 		}},
@@ -715,11 +732,9 @@ func TestInsertShardedGeo(t *testing.T) {
 		t.Fatal(err)
 	}
 	vc.ExpectLog(t, []string{
-		// ExecutePre proves that keyspace ids are generated, and that they are inserted into the lookup.
-		`ExecutePre insert into lkp(id, region, toc) values(:id0, :region0, :toc0), (:id1, :region1, :toc1) ` +
+		`Execute insert into id_idx(id, keyspace_id) values(:id0, :keyspace_id0), (:id1, :keyspace_id1) ` +
 			`id0: type:INT64 value:"1" id1: type:INT64 value:"1" ` +
-			`region0: type:INT64 value:"1" region1: type:INT64 value:"255" ` +
-			`toc0: type:VARBINARY value:"\001\026k@\264J\272K\326" toc1: type:VARBINARY value:"\377\026k@\264J\272K\326"  true`,
+			`keyspace_id0: type:VARBINARY value:"\001\026k@\264J\272K\326" keyspace_id1: type:VARBINARY value:"\377\026k@\264J\272K\326"  true`,
 		`ResolveDestinations sharded [value:"0"  value:"1" ] Destinations:DestinationKeyspaceID(01166b40b44aba4bd6),DestinationKeyspaceID(ff166b40b44aba4bd6)`,
 		`ExecuteMultiShard sharded.20-: prefix mid1 suffix /* vtgate:: keyspace_id:01166b40b44aba4bd6 */ ` +
 			`{_id0: type:INT64 value:"1" _id1: type:INT64 value:"1" ` +
@@ -922,104 +937,6 @@ func TestInsertShardedIgnoreOwned(t *testing.T) {
 	})
 }
 
-func TestInsertIgnoreGeo(t *testing.T) {
-	invschema := &vschemapb.SrvVSchema{
-		Keyspaces: map[string]*vschemapb.Keyspace{
-			"sharded": {
-				Sharded: true,
-				Vindexes: map[string]*vschemapb.Vindex{
-					"geo": {
-						Type: "region_experimental",
-						Params: map[string]string{
-							"region_bytes": "1",
-							"table":        "lkp",
-							"from":         "id,region",
-							"to":           "toc",
-						},
-						Owner: "t1",
-					},
-				},
-				Tables: map[string]*vschemapb.Table{
-					"t1": {
-						ColumnVindexes: []*vschemapb.ColumnVindex{{
-							Name:    "geo",
-							Columns: []string{"id", "region"},
-						}},
-					},
-				},
-			},
-		},
-	}
-	vs, err := vindexes.BuildVSchema(invschema)
-	if err != nil {
-		t.Fatal(err)
-	}
-	ks := vs.Keyspaces["sharded"]
-
-	ins := NewInsert(
-		InsertShardedIgnore,
-		ks.Keyspace,
-		[]sqltypes.PlanValue{{
-			// colVindex columns: id, region
-			Values: []sqltypes.PlanValue{{
-				// rows for id
-				Values: []sqltypes.PlanValue{{
-					Value: sqltypes.NewInt64(1),
-				}, {
-					Value: sqltypes.NewInt64(2),
-				}},
-			}, {
-				// rows for region
-				Values: []sqltypes.PlanValue{{
-					Value: sqltypes.NewInt64(1),
-				}, {
-					Value: sqltypes.NewInt64(2),
-				}},
-			}},
-		}},
-		ks.Tables["t1"],
-		"prefix",
-		[]string{" mid1", " mid2"},
-		" suffix",
-	)
-
-	ksid0 := sqltypes.MakeTestResult(
-		sqltypes.MakeTestFields(
-			"to",
-			"varbinary",
-		),
-		"\x00",
-	)
-	noresult := &sqltypes.Result{}
-	vc := &loggingVCursor{
-		shards:       []string{"-20", "20-"},
-		shardForKsid: []string{"20-", "-20"},
-		results: []*sqltypes.Result{
-			// insert lkp
-			noresult,
-			// fail one verification (row 2)
-			ksid0,
-			noresult,
-		},
-	}
-	_, err = ins.Execute(vc, map[string]*querypb.BindVariable{}, false)
-	if err != nil {
-		t.Fatal(err)
-	}
-	vc.ExpectLog(t, []string{
-		`ExecutePre insert ignore into lkp(id, region, toc) values(:id0, :region0, :toc0), (:id1, :region1, :toc1) ` +
-			`id0: type:INT64 value:"1" id1: type:INT64 value:"2" ` +
-			`region0: type:INT64 value:"1" region1: type:INT64 value:"2" ` +
-			`toc0: type:VARBINARY value:"\001\026k@\264J\272K\326" toc1: type:VARBINARY value:"\002\006\347\352\"\316\222p\217"  true`,
-		// Row 2 will fail verification. This is what we're testing. The second row should not get inserted.
-		`ExecutePre select id from lkp where id = :id and toc = :toc id: type:INT64 value:"1" toc: type:VARBINARY value:"\001\026k@\264J\272K\326"  false`,
-		`ExecutePre select id from lkp where id = :id and toc = :toc id: type:INT64 value:"2" toc: type:VARBINARY value:"\002\006\347\352\"\316\222p\217"  false`,
-		`ResolveDestinations sharded [value:"0" ] Destinations:DestinationKeyspaceID(01166b40b44aba4bd6)`,
-		`ExecuteMultiShard sharded.20-: prefix mid1 suffix /* vtgate:: keyspace_id:01166b40b44aba4bd6 */ ` +
-			`{_id0: type:INT64 value:"1" _region0: type:INT64 value:"1" } true true`,
-	})
-}
-
 func TestInsertShardedIgnoreOwnedWithNull(t *testing.T) {
 	invschema := &vschemapb.SrvVSchema{
 		Keyspaces: map[string]*vschemapb.Keyspace{
@@ -1270,91 +1187,6 @@ func TestInsertShardedUnownedVerify(t *testing.T) {
 	})
 }
 
-func TestInsertUnownedGeo(t *testing.T) {
-	invschema := &vschemapb.SrvVSchema{
-		Keyspaces: map[string]*vschemapb.Keyspace{
-			"sharded": {
-				Sharded: true,
-				Vindexes: map[string]*vschemapb.Vindex{
-					"primary": {
-						Type: "hash",
-					},
-					"geo": {
-						Type: "region_experimental",
-						Params: map[string]string{
-							"region_bytes": "1",
-							"table":        "lkp",
-							"from":         "other_id,region",
-							"to":           "toc",
-						},
-					},
-				},
-				Tables: map[string]*vschemapb.Table{
-					"t1": {
-						ColumnVindexes: []*vschemapb.ColumnVindex{{
-							Name:    "primary",
-							Columns: []string{"id"},
-						}, {
-							Name:    "geo",
-							Columns: []string{"other_id", "region"},
-						}},
-					},
-				},
-			},
-		},
-	}
-	vs, err := vindexes.BuildVSchema(invschema)
-	if err != nil {
-		t.Fatal(err)
-	}
-	ks := vs.Keyspaces["sharded"]
-
-	ins := NewInsert(
-		InsertSharded,
-		ks.Keyspace,
-		[]sqltypes.PlanValue{{
-			// colVindex columns: id
-			Values: []sqltypes.PlanValue{{
-				// rows for id
-				Values: []sqltypes.PlanValue{{
-					Value: sqltypes.NewInt64(1),
-				}},
-			}},
-		}, {
-			// colVindex columns: other_id, region
-			Values: []sqltypes.PlanValue{{
-				// rows for other_id
-				Values: []sqltypes.PlanValue{{
-					Value: sqltypes.NewInt64(2),
-				}},
-			}, {
-				// rows for region
-				Values: []sqltypes.PlanValue{{
-					Value: sqltypes.NewInt64(3),
-				}},
-			}},
-		}},
-		ks.Tables["t1"],
-		"prefix",
-		[]string{" mid1"},
-		" suffix",
-	)
-
-	noresult := &sqltypes.Result{}
-	vc := &loggingVCursor{
-		shards: []string{"-20", "20-"},
-		results: []*sqltypes.Result{
-			// fail verification
-			noresult,
-		},
-	}
-	_, err = ins.Execute(vc, map[string]*querypb.BindVariable{}, false)
-	assert.EqualError(t, err, "execInsertSharded: getInsertShardedRoute: values [[INT64(2) INT64(3)]] for column [other_id region] does not map to keyspace ids")
-	vc.ExpectLog(t, []string{
-		`ExecutePre select other_id from lkp where other_id = :other_id and toc = :toc other_id: type:INT64 value:"2" toc: type:VARBINARY value:"\026k@\264J\272K\326"  false`,
-	})
-}
-
 func TestInsertShardedIgnoreUnownedVerify(t *testing.T) {
 	invschema := &vschemapb.SrvVSchema{
 		Keyspaces: map[string]*vschemapb.Keyspace{
diff --git a/go/vt/vtgate/engine/route_test.go b/go/vt/vtgate/engine/route_test.go
index 5e34bb5f4c..5855e2e587 100644
--- a/go/vt/vtgate/engine/route_test.go
+++ b/go/vt/vtgate/engine/route_test.go
@@ -119,7 +119,7 @@ func TestSelectEqualUnique(t *testing.T) {
 		"dummy_select",
 		"dummy_select_field",
 	)
-	sel.Vindex = vindex
+	sel.Vindex = vindex.(vindexes.SingleColumn)
 	sel.Values = []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}}
 
 	vc := &loggingVCursor{
@@ -164,7 +164,7 @@ func TestSelectEqualUniqueScatter(t *testing.T) {
 		"dummy_select",
 		"dummy_select_field",
 	)
-	sel.Vindex = vindex
+	sel.Vindex = vindex.(vindexes.SingleColumn)
 	sel.Values = []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}}
 
 	vc := &loggingVCursor{
@@ -208,7 +208,7 @@ func TestSelectEqual(t *testing.T) {
 		"dummy_select",
 		"dummy_select_field",
 	)
-	sel.Vindex = vindex
+	sel.Vindex = vindex.(vindexes.SingleColumn)
 	sel.Values = []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}}
 
 	vc := &loggingVCursor{
@@ -264,7 +264,7 @@ func TestSelectEqualNoRoute(t *testing.T) {
 		"dummy_select",
 		"dummy_select_field",
 	)
-	sel.Vindex = vindex
+	sel.Vindex = vindex.(vindexes.SingleColumn)
 	sel.Values = []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}}
 
 	vc := &loggingVCursor{shards: []string{"-20", "20-"}}
@@ -301,7 +301,7 @@ func TestSelectINUnique(t *testing.T) {
 		"dummy_select",
 		"dummy_select_field",
 	)
-	sel.Vindex = vindex
+	sel.Vindex = vindex.(vindexes.SingleColumn)
 	sel.Values = []sqltypes.PlanValue{{
 		Values: []sqltypes.PlanValue{{
 			Value: sqltypes.NewInt64(1),
@@ -357,7 +357,7 @@ func TestSelectINNonUnique(t *testing.T) {
 		"dummy_select",
 		"dummy_select_field",
 	)
-	sel.Vindex = vindex
+	sel.Vindex = vindex.(vindexes.SingleColumn)
 	sel.Values = []sqltypes.PlanValue{{
 		Values: []sqltypes.PlanValue{{
 			Value: sqltypes.NewInt64(1),
@@ -542,7 +542,7 @@ func TestRouteGetFields(t *testing.T) {
 		"dummy_select",
 		"dummy_select_field",
 	)
-	sel.Vindex = vindex
+	sel.Vindex = vindex.(vindexes.SingleColumn)
 	sel.Values = []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}}
 
 	vc := &loggingVCursor{shards: []string{"-20", "20-"}}
diff --git a/go/vt/vtgate/engine/update_test.go b/go/vt/vtgate/engine/update_test.go
index fcd51942ff..d21f62990f 100644
--- a/go/vt/vtgate/engine/update_test.go
+++ b/go/vt/vtgate/engine/update_test.go
@@ -66,7 +66,7 @@ func TestUpdateEqual(t *testing.T) {
 			Sharded: true,
 		},
 		Query:  "dummy_update",
-		Vindex: vindex,
+		Vindex: vindex.(vindexes.SingleColumn),
 		Values: []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}},
 	}
 
@@ -95,7 +95,7 @@ func TestUpdateScatter(t *testing.T) {
 			Sharded: true,
 		},
 		Query:  "dummy_update",
-		Vindex: vindex,
+		Vindex: vindex.(vindexes.SingleColumn),
 		Values: []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}},
 	}
 
@@ -118,7 +118,7 @@ func TestUpdateScatter(t *testing.T) {
 			Sharded: true,
 		},
 		Query:                "dummy_update",
-		Vindex:               vindex,
+		Vindex:               vindex.(vindexes.SingleColumn),
 		Values:               []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}},
 		MultiShardAutocommit: true,
 	}
@@ -148,7 +148,7 @@ func TestUpdateEqualNoRoute(t *testing.T) {
 			Sharded: true,
 		},
 		Query:  "dummy_update",
-		Vindex: vindex,
+		Vindex: vindex.(vindexes.SingleColumn),
 		Values: []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}},
 	}
 
@@ -177,7 +177,7 @@ func TestUpdateEqualNoScatter(t *testing.T) {
 			Sharded: true,
 		},
 		Query:  "dummy_update",
-		Vindex: vindex,
+		Vindex: vindex.(vindexes.SingleColumn),
 		Values: []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}},
 	}
 
@@ -192,7 +192,7 @@ func TestUpdateEqualChangedVindex(t *testing.T) {
 		Opcode:   UpdateEqual,
 		Keyspace: ks.Keyspace,
 		Query:    "dummy_update",
-		Vindex:   ks.Vindexes["hash"],
+		Vindex:   ks.Vindexes["hash"].(vindexes.SingleColumn),
 		Values:   []sqltypes.PlanValue{{Value: sqltypes.NewInt64(1)}},
 		ChangedVindexValues: map[string][]sqltypes.PlanValue{
 			"twocol": {{
diff --git a/go/vt/vtgate/engine/vindex_func_test.go b/go/vt/vtgate/engine/vindex_func_test.go
index 917fc73f1b..338ca9f7dd 100644
--- a/go/vt/vtgate/engine/vindex_func_test.go
+++ b/go/vt/vtgate/engine/vindex_func_test.go
@@ -246,7 +246,7 @@ func TestFieldOrder(t *testing.T) {
 	}
 }
 
-func testVindexFunc(v vindexes.Vindex) *VindexFunc {
+func testVindexFunc(v vindexes.SingleColumn) *VindexFunc {
 	return &VindexFunc{
 		Fields: sqltypes.MakeTestFields("id|keyspace_id|range_start|range_end", "varbinary|varbinary|varbinary|varbinary"),
 		Cols:   []int{0, 1, 2, 3},
diff --git a/go/vt/vtgate/planbuilder/route_option_test.go b/go/vt/vtgate/planbuilder/route_option_test.go
index 425aa3d34f..c39dfe48b0 100644
--- a/go/vt/vtgate/planbuilder/route_option_test.go
+++ b/go/vt/vtgate/planbuilder/route_option_test.go
@@ -168,10 +168,11 @@ func TestIsBetterThan(t *testing.T) {
 		case 2:
 			v, _ = newLookupIndex("", nil)
 		}
+		single, _ := v.(vindexes.SingleColumn)
 		return &routeOption{
 			eroute: &engine.Route{
 				Opcode: opt,
-				Vindex: v,
+				Vindex: single,
 			},
 		}
 	}
diff --git a/go/vt/vtgate/planbuilder/symtab_test.go b/go/vt/vtgate/planbuilder/symtab_test.go
index 34006ff4b2..81e85e889b 100644
--- a/go/vt/vtgate/planbuilder/symtab_test.go
+++ b/go/vt/vtgate/planbuilder/symtab_test.go
@@ -28,6 +28,8 @@ func TestSymtabAddVSchemaTable(t *testing.T) {
 	tname := sqlparser.TableName{Name: sqlparser.NewTableIdent("t")}
 	rb := &route{}
 
+	null, _ := vindexes.CreateVindex("null", "null", nil)
+
 	tcases := []struct {
 		in            []*vindexes.Table
 		authoritative bool
@@ -49,6 +51,7 @@ func TestSymtabAddVSchemaTable(t *testing.T) {
 		in: []*vindexes.Table{{
 			ColumnVindexes: []*vindexes.ColumnVindex{{
 				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("C1")},
+				Vindex:  null,
 			}},
 			Columns: []vindexes.Column{{
 				Name: sqlparser.NewColIdent("C1"),
@@ -66,6 +69,7 @@ func TestSymtabAddVSchemaTable(t *testing.T) {
 					sqlparser.NewColIdent("C1"),
 					sqlparser.NewColIdent("C2"),
 				},
+				Vindex: null,
 			}},
 			Columns: []vindexes.Column{{
 				Name: sqlparser.NewColIdent("C1"),
@@ -94,6 +98,7 @@ func TestSymtabAddVSchemaTable(t *testing.T) {
 		in: []*vindexes.Table{{
 			ColumnVindexes: []*vindexes.ColumnVindex{{
 				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("C1")},
+				Vindex:  null,
 			}},
 			Columns: []vindexes.Column{{
 				Name: sqlparser.NewColIdent("C2"),
@@ -109,6 +114,7 @@ func TestSymtabAddVSchemaTable(t *testing.T) {
 					sqlparser.NewColIdent("C1"),
 					sqlparser.NewColIdent("C2"),
 				},
+				Vindex: null,
 			}},
 		}},
 		authoritative: false,
@@ -145,12 +151,14 @@ func TestSymtabAddVSchemaTable(t *testing.T) {
 				Columns: []sqlparser.ColIdent{
 					sqlparser.NewColIdent("C1"),
 				},
+				Vindex: null,
 			}},
 		}, {
 			ColumnVindexes: []*vindexes.ColumnVindex{{
 				Columns: []sqlparser.ColIdent{
 					sqlparser.NewColIdent("C2"),
 				},
+				Vindex: null,
 			}},
 		}},
 		authoritative: false,
@@ -162,10 +170,12 @@ func TestSymtabAddVSchemaTable(t *testing.T) {
 				Columns: []sqlparser.ColIdent{
 					sqlparser.NewColIdent("C1"),
 				},
+				Vindex: null,
 			}, {
 				Columns: []sqlparser.ColIdent{
 					sqlparser.NewColIdent("C2"),
 				},
+				Vindex: null,
 			}},
 		}},
 		authoritative: false,
@@ -246,6 +256,7 @@ func TestSymtabAddVSchemaTable(t *testing.T) {
 				Columns: []sqlparser.ColIdent{
 					sqlparser.NewColIdent("C2"),
 				},
+				Vindex: null,
 			}},
 		}},
 		err: "column C2 not found in t",
diff --git a/go/vt/vtgate/vindexes/binary_test.go b/go/vt/vtgate/vindexes/binary_test.go
index a6fe555558..ac9d566609 100644
--- a/go/vt/vtgate/vindexes/binary_test.go
+++ b/go/vt/vtgate/vindexes/binary_test.go
@@ -26,10 +26,11 @@ import (
 	"vitess.io/vitess/go/vt/key"
 )
 
-var binOnlyVindex Vindex
+var binOnlyVindex SingleColumn
 
 func init() {
-	binOnlyVindex, _ = CreateVindex("binary", "binary_varchar", nil)
+	vindex, _ := CreateVindex("binary", "binary_varchar", nil)
+	binOnlyVindex = vindex.(SingleColumn)
 }
 
 func TestBinaryCost(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/binarymd5_test.go b/go/vt/vtgate/vindexes/binarymd5_test.go
index e3a133c1fa..f959fb4cd1 100644
--- a/go/vt/vtgate/vindexes/binarymd5_test.go
+++ b/go/vt/vtgate/vindexes/binarymd5_test.go
@@ -26,10 +26,11 @@ import (
 	"vitess.io/vitess/go/vt/key"
 )
 
-var binVindex Vindex
+var binVindex SingleColumn
 
 func init() {
-	binVindex, _ = CreateVindex("binary_md5", "binary_md5_varchar", nil)
+	vindex, _ := CreateVindex("binary_md5", "binary_md5_varchar", nil)
+	binVindex = vindex.(SingleColumn)
 }
 
 func TestBinaryMD5Cost(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/consistent_lookup_test.go b/go/vt/vtgate/vindexes/consistent_lookup_test.go
index e1e571d07e..bb25c20427 100644
--- a/go/vt/vtgate/vindexes/consistent_lookup_test.go
+++ b/go/vt/vtgate/vindexes/consistent_lookup_test.go
@@ -412,7 +412,7 @@ func TestConsistentLookupNoUpdate(t *testing.T) {
 	vc.verifyLog(t, []string{})
 }
 
-func createConsistentLookup(t *testing.T, name string) Vindex {
+func createConsistentLookup(t *testing.T, name string) SingleColumn {
 	t.Helper()
 	l, err := CreateVindex(name, name, map[string]string{
 		"table": "t",
@@ -429,7 +429,7 @@ func createConsistentLookup(t *testing.T, name string) Vindex {
 	if err := l.(WantOwnerInfo).SetOwnerInfo("ks", "t1", cols); err != nil {
 		t.Fatal(err)
 	}
-	return l
+	return l.(SingleColumn)
 }
 
 type loggingVCursor struct {
diff --git a/go/vt/vtgate/vindexes/hash_test.go b/go/vt/vtgate/vindexes/hash_test.go
index c0e811a361..847cf1968f 100644
--- a/go/vt/vtgate/vindexes/hash_test.go
+++ b/go/vt/vtgate/vindexes/hash_test.go
@@ -25,14 +25,14 @@ import (
 	"vitess.io/vitess/go/vt/key"
 )
 
-var hash Vindex
+var hash SingleColumn
 
 func init() {
 	hv, err := CreateVindex("hash", "nn", map[string]string{"Table": "t", "Column": "c"})
 	if err != nil {
 		panic(err)
 	}
-	hash = hv
+	hash = hv.(SingleColumn)
 }
 
 func TestHashCost(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/lookup_hash_unique_test.go b/go/vt/vtgate/vindexes/lookup_hash_unique_test.go
index bd67e77523..27ea2cb4e2 100644
--- a/go/vt/vtgate/vindexes/lookup_hash_unique_test.go
+++ b/go/vt/vtgate/vindexes/lookup_hash_unique_test.go
@@ -31,12 +31,13 @@ func TestLookupHashUniqueNew(t *testing.T) {
 		t.Errorf("Create(lookup, false): %v, want %v", got, want)
 	}
 
-	l, _ = CreateVindex("lookup_hash_unique", "lookup_hash_unique", map[string]string{
+	vindex, _ := CreateVindex("lookup_hash_unique", "lookup_hash_unique", map[string]string{
 		"table":      "t",
 		"from":       "fromc",
 		"to":         "toc",
 		"write_only": "true",
 	})
+	l = vindex.(SingleColumn)
 	if want, got := l.(*LookupHashUnique).writeOnly, true; got != want {
 		t.Errorf("Create(lookup, false): %v, want %v", got, want)
 	}
diff --git a/go/vt/vtgate/vindexes/lookup_test.go b/go/vt/vtgate/vindexes/lookup_test.go
index ca26ce61e9..12865b8fb7 100644
--- a/go/vt/vtgate/vindexes/lookup_test.go
+++ b/go/vt/vtgate/vindexes/lookup_test.go
@@ -185,7 +185,7 @@ func TestLookupNonUniqueMap(t *testing.T) {
 }
 
 func TestLookupNonUniqueMapAutocommit(t *testing.T) {
-	lookupNonUnique, err := CreateVindex("lookup", "lookup", map[string]string{
+	vindex, err := CreateVindex("lookup", "lookup", map[string]string{
 		"table":      "t",
 		"from":       "fromc",
 		"to":         "toc",
@@ -194,6 +194,7 @@ func TestLookupNonUniqueMapAutocommit(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	lookupNonUnique := vindex.(SingleColumn)
 	vc := &vcursor{numRows: 2}
 
 	got, err := lookupNonUnique.Map(vc, []sqltypes.Value{sqltypes.NewInt64(1), sqltypes.NewInt64(2)})
@@ -325,7 +326,7 @@ func TestLookupNonUniqueVerify(t *testing.T) {
 }
 
 func TestLookupNonUniqueVerifyAutocommit(t *testing.T) {
-	lookupNonUnique, err := CreateVindex("lookup", "lookup", map[string]string{
+	vindex, err := CreateVindex("lookup", "lookup", map[string]string{
 		"table":      "t",
 		"from":       "fromc",
 		"to":         "toc",
@@ -334,6 +335,7 @@ func TestLookupNonUniqueVerifyAutocommit(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	lookupNonUnique := vindex.(SingleColumn)
 	vc := &vcursor{numRows: 1}
 
 	_, err = lookupNonUnique.Verify(vc, []sqltypes.Value{sqltypes.NewInt64(1), sqltypes.NewInt64(2)}, [][]byte{[]byte("test1"), []byte("test2")})
@@ -549,7 +551,7 @@ func TestLookupNonUniqueUpdate(t *testing.T) {
 	}
 }
 
-func createLookup(t *testing.T, name string, writeOnly bool) Vindex {
+func createLookup(t *testing.T, name string, writeOnly bool) SingleColumn {
 	t.Helper()
 	write := "false"
 	if writeOnly {
@@ -564,5 +566,5 @@ func createLookup(t *testing.T, name string, writeOnly bool) Vindex {
 	if err != nil {
 		t.Fatal(err)
 	}
-	return l
+	return l.(SingleColumn)
 }
diff --git a/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash_test.go b/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash_test.go
index dc87eaf6aa..abe193974e 100644
--- a/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash_test.go
+++ b/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash_test.go
@@ -82,7 +82,7 @@ func TestLookupUnicodeLooseMD5HashMap(t *testing.T) {
 }
 
 func TestLookupUnicodeLooseMD5HashMapAutocommit(t *testing.T) {
-	lookupNonUnique, err := CreateVindex("lookup_unicodeloosemd5_hash", "lookup", map[string]string{
+	vindex, err := CreateVindex("lookup_unicodeloosemd5_hash", "lookup", map[string]string{
 		"table":      "t",
 		"from":       "fromc",
 		"to":         "toc",
@@ -92,6 +92,7 @@ func TestLookupUnicodeLooseMD5HashMapAutocommit(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	lookupNonUnique := vindex.(SingleColumn)
 	vc := &vcursor{numRows: 2}
 
 	got, err := lookupNonUnique.Map(vc, []sqltypes.Value{sqltypes.NewInt64(10), sqltypes.NewInt64(20)})
@@ -229,7 +230,7 @@ func TestLookupUnicodeLooseMD5HashVerify(t *testing.T) {
 }
 
 func TestLookupUnicodeLooseMD5HashVerifyAutocommit(t *testing.T) {
-	lookupNonUnique, err := CreateVindex("lookup_unicodeloosemd5_hash", "lookup", map[string]string{
+	vindex, err := CreateVindex("lookup_unicodeloosemd5_hash", "lookup", map[string]string{
 		"table":      "t",
 		"from":       "fromc",
 		"to":         "toc",
@@ -238,6 +239,7 @@ func TestLookupUnicodeLooseMD5HashVerifyAutocommit(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	lookupNonUnique := vindex.(SingleColumn)
 	vc := &vcursor{numRows: 1}
 
 	_, err = lookupNonUnique.Verify(vc, []sqltypes.Value{sqltypes.NewInt64(10), sqltypes.NewInt64(20)},
diff --git a/go/vt/vtgate/vindexes/lookup_unique_test.go b/go/vt/vtgate/vindexes/lookup_unique_test.go
index 5e33bf370a..cb9ab2bed8 100644
--- a/go/vt/vtgate/vindexes/lookup_unique_test.go
+++ b/go/vt/vtgate/vindexes/lookup_unique_test.go
@@ -33,12 +33,13 @@ func TestLookupUniqueNew(t *testing.T) {
 		t.Errorf("Create(lookup, false): %v, want %v", got, want)
 	}
 
-	l, _ = CreateVindex("lookup_unique", "lookup_unique", map[string]string{
+	vindex, _ := CreateVindex("lookup_unique", "lookup_unique", map[string]string{
 		"table":      "t",
 		"from":       "fromc",
 		"to":         "toc",
 		"write_only": "true",
 	})
+	l = vindex.(SingleColumn)
 	if want, got := l.(*LookupUnique).writeOnly, true; got != want {
 		t.Errorf("Create(lookup, false): %v, want %v", got, want)
 	}
diff --git a/go/vt/vtgate/vindexes/null_test.go b/go/vt/vtgate/vindexes/null_test.go
index f92855edca..11568f9eb5 100644
--- a/go/vt/vtgate/vindexes/null_test.go
+++ b/go/vt/vtgate/vindexes/null_test.go
@@ -25,14 +25,14 @@ import (
 	"vitess.io/vitess/go/vt/key"
 )
 
-var null Vindex
+var null SingleColumn
 
 func init() {
 	hv, err := CreateVindex("null", "nn", map[string]string{"Table": "t", "Column": "c"})
 	if err != nil {
 		panic(err)
 	}
-	null = hv
+	null = hv.(SingleColumn)
 }
 
 func TestNullCost(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/numeric_static_map_test.go b/go/vt/vtgate/vindexes/numeric_static_map_test.go
index e81abe331d..94035320f0 100644
--- a/go/vt/vtgate/vindexes/numeric_static_map_test.go
+++ b/go/vt/vtgate/vindexes/numeric_static_map_test.go
@@ -28,10 +28,14 @@ import (
 
 // createVindex creates the "numeric_static_map" vindex object which is used by
 // each test.
-func createVindex() (Vindex, error) {
+func createVindex() (SingleColumn, error) {
 	m := make(map[string]string)
 	m["json_path"] = "testdata/numeric_static_map_test.json"
-	return CreateVindex("numeric_static_map", "numericStaticMap", m)
+	vindex, err := CreateVindex("numeric_static_map", "numericStaticMap", m)
+	if err != nil {
+		panic(err)
+	}
+	return vindex.(SingleColumn), nil
 }
 
 func TestNumericStaticMapCost(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/numeric_test.go b/go/vt/vtgate/vindexes/numeric_test.go
index 7ad9a357f6..3d0d053241 100644
--- a/go/vt/vtgate/vindexes/numeric_test.go
+++ b/go/vt/vtgate/vindexes/numeric_test.go
@@ -26,10 +26,11 @@ import (
 	"vitess.io/vitess/go/vt/key"
 )
 
-var numeric Vindex
+var numeric SingleColumn
 
 func init() {
-	numeric, _ = CreateVindex("numeric", "num", nil)
+	vindex, _ := CreateVindex("numeric", "num", nil)
+	numeric = vindex.(SingleColumn)
 }
 
 func TestNumericCost(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/region_experimental_test.go b/go/vt/vtgate/vindexes/region_experimental_test.go
index ef85c65b90..f1162f4361 100644
--- a/go/vt/vtgate/vindexes/region_experimental_test.go
+++ b/go/vt/vtgate/vindexes/region_experimental_test.go
@@ -25,23 +25,32 @@ import (
 	"vitess.io/vitess/go/vt/key"
 )
 
-func TestRegionExperimentalMapMulti1(t *testing.T) {
+func TestRegionExperimentalMisc(t *testing.T) {
 	ge, err := createRegionVindex(t, "region_experimental", "f1,f2", 1)
 	assert.NoError(t, err)
-	got, err := ge.(MultiColumn).MapMulti(nil, [][]sqltypes.Value{{
+	assert.Equal(t, 1, ge.Cost())
+	assert.Equal(t, "region_experimental", ge.String())
+	assert.True(t, ge.IsUnique())
+}
+
+func TestRegionExperimentalMap(t *testing.T) {
+	vindex, err := createRegionVindex(t, "region_experimental", "f1,f2", 1)
+	assert.NoError(t, err)
+	ge := vindex.(MultiColumn)
+	got, err := ge.Map(nil, [][]sqltypes.Value{{
 		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
 	}, {
-		sqltypes.NewInt64(1), sqltypes.NewInt64(255),
+		sqltypes.NewInt64(255), sqltypes.NewInt64(1),
 	}, {
-		sqltypes.NewInt64(1), sqltypes.NewInt64(256),
+		sqltypes.NewInt64(256), sqltypes.NewInt64(1),
 	}, {
 		// Invalid length.
 		sqltypes.NewInt64(1),
 	}, {
-		// Invalid id.
+		// Invalid region.
 		sqltypes.NewVarBinary("abcd"), sqltypes.NewInt64(256),
 	}, {
-		// Invalid region.
+		// Invalid id.
 		sqltypes.NewInt64(1), sqltypes.NewVarBinary("abcd"),
 	}})
 	assert.NoError(t, err)
@@ -58,16 +67,17 @@ func TestRegionExperimentalMapMulti1(t *testing.T) {
 }
 
 func TestRegionExperimentalMapMulti2(t *testing.T) {
-	ge, err := createRegionVindex(t, "region_experimental", "f1,f2", 2)
+	vindex, err := createRegionVindex(t, "region_experimental", "f1,f2", 2)
 	assert.NoError(t, err)
-	got, err := ge.(MultiColumn).MapMulti(nil, [][]sqltypes.Value{{
+	ge := vindex.(MultiColumn)
+	got, err := ge.Map(nil, [][]sqltypes.Value{{
 		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
 	}, {
-		sqltypes.NewInt64(1), sqltypes.NewInt64(255),
+		sqltypes.NewInt64(255), sqltypes.NewInt64(1),
 	}, {
-		sqltypes.NewInt64(1), sqltypes.NewInt64(256),
+		sqltypes.NewInt64(256), sqltypes.NewInt64(1),
 	}, {
-		sqltypes.NewInt64(1), sqltypes.NewInt64(0x10000),
+		sqltypes.NewInt64(0x10000), sqltypes.NewInt64(1),
 	}})
 	assert.NoError(t, err)
 
@@ -81,15 +91,12 @@ func TestRegionExperimentalMapMulti2(t *testing.T) {
 }
 
 func TestRegionExperimentalVerifyMulti(t *testing.T) {
-
-	ge, err := createRegionVindex(t, "region_experimental", "f1,f2", 1)
+	vindex, err := createRegionVindex(t, "region_experimental", "f1,f2", 1)
 	assert.NoError(t, err)
+	ge := vindex.(MultiColumn)
 	vals := [][]sqltypes.Value{{
 		// One for match
 		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
-	}, {
-		// One for mismatch by lookup
-		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
 	}, {
 		// One for mismatch
 		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
@@ -98,27 +105,14 @@ func TestRegionExperimentalVerifyMulti(t *testing.T) {
 		sqltypes.NewInt64(1),
 	}}
 	ksids := [][]byte{
-		[]byte("\x01\x16k@\xb4J\xbaK\xd6"),
 		[]byte("\x01\x16k@\xb4J\xbaK\xd6"),
 		[]byte("no match"),
 		[]byte(""),
 	}
-	vc := &loggingVCursor{}
-	vc.AddResult(makeTestResult(1), nil)
-	// The second value should return a mismatch.
-	vc.AddResult(&sqltypes.Result{}, nil)
-	vc.AddResult(makeTestResult(1), nil)
-	vc.AddResult(makeTestResult(1), nil)
 
-	want := []bool{true, false, false, false}
-	got, err := ge.(MultiColumn).VerifyMulti(vc, vals, ksids)
+	want := []bool{true, false, false}
+	got, err := ge.Verify(nil, vals, ksids)
 	assert.NoError(t, err)
-	vc.verifyLog(t, []string{
-		"ExecutePre select f1 from t where f1 = :f1 and toc = :toc [{f1 1} {toc \x01\x16k@\xb4J\xbaK\xd6}] false",
-		"ExecutePre select f1 from t where f1 = :f1 and toc = :toc [{f1 1} {toc \x01\x16k@\xb4J\xbaK\xd6}] false",
-		"ExecutePre select f1 from t where f1 = :f1 and toc = :toc [{f1 1} {toc no match}] false",
-		"ExecutePre select f1 from t where f1 = :f1 and toc = :toc [{f1 1} {toc }] false",
-	})
 	assert.Equal(t, want, got)
 }
 
@@ -127,8 +121,6 @@ func TestRegionExperimentalCreateErrors(t *testing.T) {
 	assert.EqualError(t, err, "region_bits must be 1 or 2: 3")
 	_, err = CreateVindex("region_experimental", "region_experimental", nil)
 	assert.EqualError(t, err, "region_experimental missing region_bytes param")
-	_, err = createRegionVindex(t, "region_experimental", "f1", 2)
-	assert.EqualError(t, err, "two columns are required for region_experimental: [f1]")
 }
 
 func createRegionVindex(t *testing.T, name, from string, rb int) (Vindex, error) {
diff --git a/go/vt/vtgate/vindexes/reverse_bits_test.go b/go/vt/vtgate/vindexes/reverse_bits_test.go
index bfd5a1c8b3..a135b8ca56 100644
--- a/go/vt/vtgate/vindexes/reverse_bits_test.go
+++ b/go/vt/vtgate/vindexes/reverse_bits_test.go
@@ -25,14 +25,14 @@ import (
 	"vitess.io/vitess/go/vt/key"
 )
 
-var reverseBits Vindex
+var reverseBits SingleColumn
 
 func init() {
 	hv, err := CreateVindex("reverse_bits", "rr", map[string]string{"Table": "t", "Column": "c"})
 	if err != nil {
 		panic(err)
 	}
-	reverseBits = hv
+	reverseBits = hv.(SingleColumn)
 }
 
 func TestReverseBitsCost(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/unicodeloosemd5_test.go b/go/vt/vtgate/vindexes/unicodeloosemd5_test.go
index 9c51f178b1..89bcc19fbb 100644
--- a/go/vt/vtgate/vindexes/unicodeloosemd5_test.go
+++ b/go/vt/vtgate/vindexes/unicodeloosemd5_test.go
@@ -26,10 +26,11 @@ import (
 	"vitess.io/vitess/go/vt/key"
 )
 
-var charVindex Vindex
+var charVindex SingleColumn
 
 func init() {
-	charVindex, _ = CreateVindex("unicode_loose_md5", "utf8ch", nil)
+	vindex, _ := CreateVindex("unicode_loose_md5", "utf8ch", nil)
+	charVindex = vindex.(SingleColumn)
 }
 
 func TestUnicodeLooseMD5Cost(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/vindex_test.go b/go/vt/vtgate/vindexes/vindex_test.go
index 1c85f83a1f..c78ab192b2 100644
--- a/go/vt/vtgate/vindexes/vindex_test.go
+++ b/go/vt/vtgate/vindexes/vindex_test.go
@@ -51,12 +51,10 @@ func TestVindexMap(t *testing.T) {
 }
 
 func TestVindexVerify(t *testing.T) {
-	vc := &loggingVCursor{}
-	vc.AddResult(makeTestResult(1), nil)
 	ge, err := createRegionVindex(t, "region_experimental", "f1,f2", 1)
 	assert.NoError(t, err)
 
-	got, err := Verify(ge, vc, [][]sqltypes.Value{{
+	got, err := Verify(ge, nil, [][]sqltypes.Value{{
 		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
 	}},
 		[][]byte{
@@ -64,9 +62,6 @@ func TestVindexVerify(t *testing.T) {
 		},
 	)
 	assert.NoError(t, err)
-	vc.verifyLog(t, []string{
-		"ExecutePre select f1 from t where f1 = :f1 and toc = :toc [{f1 1} {toc \x01\x16k@\xb4J\xbaK\xd6}] false",
-	})
 
 	want := []bool{true}
 	assert.Equal(t, want, got)
diff --git a/go/vt/vtgate/vindexes/vschema_test.go b/go/vt/vtgate/vindexes/vschema_test.go
index 200636c238..5ed01c5285 100644
--- a/go/vt/vtgate/vindexes/vschema_test.go
+++ b/go/vt/vtgate/vindexes/vschema_test.go
@@ -52,7 +52,7 @@ func NewSTFU(name string, params map[string]string) (Vindex, error) {
 	return &stFU{name: name, Params: params}, nil
 }
 
-var _ Vindex = (*stFU)(nil)
+var _ SingleColumn = (*stFU)(nil)
 
 // stLN is a Lookup, NonUnique Vindex.
 type stLN struct {
@@ -73,7 +73,7 @@ func NewSTLN(name string, params map[string]string) (Vindex, error) {
 	return &stLN{name: name, Params: params}, nil
 }
 
-var _ Vindex = (*stLN)(nil)
+var _ SingleColumn = (*stLN)(nil)
 var _ Lookup = (*stLN)(nil)
 
 // stLU is a Lookup, Unique Vindex.
@@ -95,7 +95,7 @@ func NewSTLU(name string, params map[string]string) (Vindex, error) {
 	return &stLU{name: name, Params: params}, nil
 }
 
-var _ Vindex = (*stLO)(nil)
+var _ SingleColumn = (*stLO)(nil)
 var _ Lookup = (*stLO)(nil)
 var _ WantOwnerInfo = (*stLO)(nil)
 
@@ -126,7 +126,7 @@ func NewSTLO(name string, _ map[string]string) (Vindex, error) {
 	return &stLO{name: name}, nil
 }
 
-var _ Vindex = (*stLO)(nil)
+var _ SingleColumn = (*stLO)(nil)
 var _ Lookup = (*stLO)(nil)
 
 func init() {
@@ -1554,6 +1554,38 @@ func TestBuildVSchemaNotUniqueFail(t *testing.T) {
 	}
 }
 
+func TestBuildVSchemaPrimaryCannotBeOwned(t *testing.T) {
+	bad := vschemapb.SrvVSchema{
+		Keyspaces: map[string]*vschemapb.Keyspace{
+			"sharded": {
+				Sharded: true,
+				Vindexes: map[string]*vschemapb.Vindex{
+					"stlu": {
+						Type:  "stlu",
+						Owner: "t1",
+					},
+				},
+				Tables: map[string]*vschemapb.Table{
+					"t1": {
+						ColumnVindexes: []*vschemapb.ColumnVindex{
+							{
+								Column: "c1",
+								Name:   "stlu",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	got, _ := BuildVSchema(&bad)
+	err := got.Keyspaces["sharded"].Error
+	want := "primary vindex stlu cannot be owned for table t1"
+	if err == nil || err.Error() != want {
+		t.Errorf("BuildVSchema: %v, want %v", err, want)
+	}
+}
+
 func TestSequence(t *testing.T) {
 	good := vschemapb.SrvVSchema{
 		Keyspaces: map[string]*vschemapb.Keyspace{
diff --git a/go/vt/vtgate/vindexes/xxhash_test.go b/go/vt/vtgate/vindexes/xxhash_test.go
index 36c1719443..187682f972 100644
--- a/go/vt/vtgate/vindexes/xxhash_test.go
+++ b/go/vt/vtgate/vindexes/xxhash_test.go
@@ -29,14 +29,14 @@ import (
 	"vitess.io/vitess/go/vt/key"
 )
 
-var xxHash Vindex
+var xxHash SingleColumn
 
 func init() {
 	hv, err := CreateVindex("xxhash", "xxhash_name", map[string]string{"Table": "t", "Column": "c"})
 	if err != nil {
 		panic(err)
 	}
-	xxHash = hv
+	xxHash = hv.(SingleColumn)
 }
 
 func TestXXHashCost(t *testing.T) {
diff --git a/go/vt/vttablet/tabletserver/vstreamer/engine_test.go b/go/vt/vttablet/tabletserver/vstreamer/engine_test.go
index 575b146b6e..28c5da5643 100644
--- a/go/vt/vttablet/tabletserver/vstreamer/engine_test.go
+++ b/go/vt/vttablet/tabletserver/vstreamer/engine_test.go
@@ -26,7 +26,8 @@ import (
 	binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata"
 )
 
-var shardedVSchema = `{
+var (
+	shardedVSchema = `{
   "sharded": true,
   "vindexes": {
     "hash": {
@@ -45,6 +46,32 @@ var shardedVSchema = `{
   }
 }`
 
+	multicolumnVSchema = `{
+  "sharded": true,
+  "vindexes": {
+    "region_vdx": {
+      "type": "region_experimental",
+			"params": {
+				"region_bytes": "1"
+			}
+    }
+  },
+  "tables": {
+    "t1": {
+      "column_vindexes": [
+        {
+          "columns": [
+						"region",
+						"id"
+					],
+          "name": "region_vdx"
+        }
+      ]
+    }
+  }
+}`
+)
+
 func TestUpdateVSchema(t *testing.T) {
 	if testing.Short() {
 		t.Skip()
diff --git a/go/vt/vttablet/tabletserver/vstreamer/planbuilder_test.go b/go/vt/vttablet/tabletserver/vstreamer/planbuilder_test.go
index 4c70242d7f..e6d38059e9 100644
--- a/go/vt/vttablet/tabletserver/vstreamer/planbuilder_test.go
+++ b/go/vt/vttablet/tabletserver/vstreamer/planbuilder_test.go
@@ -41,8 +41,11 @@ func init() {
     "hash": {
       "type": "hash"
     },
-    "lookup": {
-      "type": "lookup"
+    "region_vdx": {
+      "type": "region_experimental",
+			"params": {
+				"region_bytes": "1"
+			}
     }
   },
   "tables": {
@@ -53,6 +56,17 @@ func init() {
           "name": "hash"
         }
       ]
+    },
+    "regional": {
+      "column_vindexes": [
+        {
+          "columns": [
+						"region",
+						"id"
+					],
+          "name": "region_vdx"
+        }
+      ]
     }
   }
 }`
@@ -177,6 +191,19 @@ func TestPlanbuilder(t *testing.T) {
 			Type: sqltypes.VarBinary,
 		}},
 	}
+	regional := &Table{
+		Name: "regional",
+		Columns: []schema.TableColumn{{
+			Name: sqlparser.NewColIdent("region"),
+			Type: sqltypes.Int64,
+		}, {
+			Name: sqlparser.NewColIdent("id"),
+			Type: sqltypes.Int64,
+		}, {
+			Name: sqlparser.NewColIdent("val"),
+			Type: sqltypes.VarBinary,
+		}},
+	}
 
 	testcases := []struct {
 		inTable *Table
@@ -210,7 +237,7 @@ func TestPlanbuilder(t *testing.T) {
 				Alias:  sqlparser.NewColIdent("val"),
 				Type:   sqltypes.VarBinary,
 			}},
-			VindexColumn: 0,
+			VindexColumns: []int{0},
 		},
 	}, {
 		inTable: t1,
@@ -267,7 +294,7 @@ func TestPlanbuilder(t *testing.T) {
 				Alias:  sqlparser.NewColIdent("id"),
 				Type:   sqltypes.Int64,
 			}},
-			VindexColumn: 1,
+			VindexColumns: []int{0},
 		},
 	}, {
 		inTable: t1,
@@ -282,11 +309,41 @@ func TestPlanbuilder(t *testing.T) {
 				Alias:  sqlparser.NewColIdent("id"),
 				Type:   sqltypes.Int64,
 			}},
-			VindexColumn: 1,
+			VindexColumns: []int{0},
 		},
 	}, {
 		inTable: t2,
 		inRule:  &binlogdatapb.Rule{Match: "/t1/"},
+	}, {
+		inTable: regional,
+		inRule:  &binlogdatapb.Rule{Match: "regional", Filter: "select val, id from regional where in_keyrange('-80')"},
+		outPlan: &Plan{
+			ColExprs: []ColExpr{{
+				ColNum: 2,
+				Alias:  sqlparser.NewColIdent("val"),
+				Type:   sqltypes.VarBinary,
+			}, {
+				ColNum: 1,
+				Alias:  sqlparser.NewColIdent("id"),
+				Type:   sqltypes.Int64,
+			}},
+			VindexColumns: []int{0, 1},
+		},
+	}, {
+		inTable: regional,
+		inRule:  &binlogdatapb.Rule{Match: "regional", Filter: "select id, keyspace_id() from regional"},
+		outPlan: &Plan{
+			ColExprs: []ColExpr{{
+				ColNum: 1,
+				Alias:  sqlparser.NewColIdent("id"),
+				Type:   sqltypes.Int64,
+			}, {
+				Alias:         sqlparser.NewColIdent("keyspace_id"),
+				Vindex:        testKSChema.Vindexes["region_vdx"],
+				VindexColumns: []int{0, 1},
+				Type:          sqltypes.VarBinary,
+			}},
+		},
 	}, {
 		inTable: t1,
 		inRule:  &binlogdatapb.Rule{Match: "/*/"},
@@ -355,10 +412,6 @@ func TestPlanbuilder(t *testing.T) {
 		inTable: t1,
 		inRule:  &binlogdatapb.Rule{Match: "t1", Filter: "select id, val from t1 where in_keyrange(1, 'hash', '-80')"},
 		outErr:  `unexpected: 1`,
-	}, {
-		inTable: t1,
-		inRule:  &binlogdatapb.Rule{Match: "t1", Filter: "select id, val from t1 where in_keyrange(none, 'hash', '-80')"},
-		outErr:  `keyrange expression does not reference a column in the select list: none`,
 	}, {
 		inTable: t1,
 		inRule:  &binlogdatapb.Rule{Match: "t1", Filter: "select id, val from t1 where in_keyrange(id, 'lookup', '-80')"},
diff --git a/go/vt/vttablet/tabletserver/vstreamer/vstreamer_test.go b/go/vt/vttablet/tabletserver/vstreamer/vstreamer_test.go
index 907514632a..a3eca41a71 100644
--- a/go/vt/vttablet/tabletserver/vstreamer/vstreamer_test.go
+++ b/go/vt/vttablet/tabletserver/vstreamer/vstreamer_test.go
@@ -284,6 +284,119 @@ func TestREKeyRange(t *testing.T) {
 	}})
 }
 
+func TestInKeyRangeMultiColumn(t *testing.T) {
+	if testing.Short() {
+		t.Skip()
+	}
+
+	execStatements(t, []string{
+		"create table t1(region int, id int, val varbinary(128), primary key(id))",
+	})
+	defer execStatements(t, []string{
+		"drop table t1",
+	})
+	engine.se.Reload(context.Background())
+
+	if err := env.SetVSchema(multicolumnVSchema); err != nil {
+		t.Fatal(err)
+	}
+	defer env.SetVSchema("{}")
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	filter := &binlogdatapb.Filter{
+		Rules: []*binlogdatapb.Rule{{
+			Match:  "t1",
+			Filter: "select id, region, val, keyspace_id() from t1 where in_keyrange('-80')",
+		}},
+	}
+	ch := startStream(ctx, t, filter, "")
+
+	// 1, 2, 3 and 5 are in shard -80.
+	// 4 and 6 are in shard 80-.
+	input := []string{
+		"begin",
+		"insert into t1 values (1, 1, 'aaa')",
+		"insert into t1 values (128, 2, 'bbb')",
+		// Stay in shard.
+		"update t1 set region = 2 where id = 1",
+		// Move from -80 to 80-.
+		"update t1 set region = 128 where id = 1",
+		// Move from 80- to -80.
+		"update t1 set region = 1 where id = 2",
+		"commit",
+	}
+	execStatements(t, input)
+	expectLog(ctx, t, input, ch, [][]string{{
+		`begin`,
+		`type:FIELD field_event:<table_name:"t1" fields:<name:"id" type:INT32 > fields:<name:"region" type:INT32 > fields:<name:"val" type:VARBINARY > fields:<name:"keyspace_id" type:VARBINARY > > `,
+		`type:ROW row_event:<table_name:"t1" row_changes:<after:<lengths:1 lengths:1 lengths:3 lengths:9 values:"11aaa\001\026k@\264J\272K\326" > > > `,
+		`type:ROW row_event:<table_name:"t1" row_changes:<before:<lengths:1 lengths:1 lengths:3 lengths:9 values:"11aaa\001\026k@\264J\272K\326" > ` +
+			`after:<lengths:1 lengths:1 lengths:3 lengths:9 values:"12aaa\002\026k@\264J\272K\326" > > > `,
+		`type:ROW row_event:<table_name:"t1" row_changes:<before:<lengths:1 lengths:1 lengths:3 lengths:9 values:"12aaa\002\026k@\264J\272K\326" > > > `,
+		`type:ROW row_event:<table_name:"t1" row_changes:<after:<lengths:1 lengths:1 lengths:3 lengths:9 values:"21bbb\001\006\347\352\"\316\222p\217" > > > `,
+		`gtid`,
+		`commit`,
+	}})
+}
+
+func TestREMultiColumnVindex(t *testing.T) {
+	if testing.Short() {
+		t.Skip()
+	}
+
+	execStatements(t, []string{
+		"create table t1(region int, id int, val varbinary(128), primary key(id))",
+	})
+	defer execStatements(t, []string{
+		"drop table t1",
+	})
+	engine.se.Reload(context.Background())
+
+	if err := env.SetVSchema(multicolumnVSchema); err != nil {
+		t.Fatal(err)
+	}
+	defer env.SetVSchema("{}")
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	filter := &binlogdatapb.Filter{
+		Rules: []*binlogdatapb.Rule{{
+			Match:  "/.*/",
+			Filter: "-80",
+		}},
+	}
+	ch := startStream(ctx, t, filter, "")
+
+	// 1, 2, 3 and 5 are in shard -80.
+	// 4 and 6 are in shard 80-.
+	input := []string{
+		"begin",
+		"insert into t1 values (1, 1, 'aaa')",
+		"insert into t1 values (128, 2, 'bbb')",
+		// Stay in shard.
+		"update t1 set region = 2 where id = 1",
+		// Move from -80 to 80-.
+		"update t1 set region = 128 where id = 1",
+		// Move from 80- to -80.
+		"update t1 set region = 1 where id = 2",
+		"commit",
+	}
+	execStatements(t, input)
+	expectLog(ctx, t, input, ch, [][]string{{
+		`begin`,
+		`type:FIELD field_event:<table_name:"t1" fields:<name:"region" type:INT32 > fields:<name:"id" type:INT32 > fields:<name:"val" type:VARBINARY > > `,
+		`type:ROW row_event:<table_name:"t1" row_changes:<after:<lengths:1 lengths:1 lengths:3 values:"11aaa" > > > `,
+		`type:ROW row_event:<table_name:"t1" row_changes:<before:<lengths:1 lengths:1 lengths:3 values:"11aaa" > after:<lengths:1 lengths:1 lengths:3 values:"21aaa" > > > `,
+		`type:ROW row_event:<table_name:"t1" row_changes:<before:<lengths:1 lengths:1 lengths:3 values:"21aaa" > > > `,
+		`type:ROW row_event:<table_name:"t1" row_changes:<after:<lengths:1 lengths:1 lengths:3 values:"12bbb" > > > `,
+		`gtid`,
+		`commit`,
+	}})
+}
+
 func TestSelectFilter(t *testing.T) {
 	if testing.Short() {
 		t.Skip()