[MySQL] Optimizer hints

This commit is contained in:
go-jet 2022-09-29 13:33:00 +02:00
parent c9967d151e
commit f772f90336
12 changed files with 207 additions and 27 deletions

View file

@ -16,11 +16,35 @@ type ClauseWithProjections interface {
Projections() ProjectionList Projections() ProjectionList
} }
// OptimizerHint provides a way to optimize query execution per-statement basis
type OptimizerHint string
type optimizerHints []OptimizerHint
func (o optimizerHints) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
if len(o) == 0 {
return
}
out.WriteString("/*+")
for i, hint := range o {
if i > 0 {
out.WriteByte(' ')
}
out.WriteString(string(hint))
}
out.WriteString("*/")
}
// ClauseSelect struct // ClauseSelect struct
type ClauseSelect struct { type ClauseSelect struct {
Distinct bool Distinct bool
DistinctOnColumns []ColumnExpression DistinctOnColumns []ColumnExpression
ProjectionList []Projection ProjectionList []Projection
// MySQL only
OptimizerHints optimizerHints
} }
// Projections returns list of projections for select clause // Projections returns list of projections for select clause
@ -32,6 +56,7 @@ func (s *ClauseSelect) Projections() ProjectionList {
func (s *ClauseSelect) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) { func (s *ClauseSelect) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
out.NewLine() out.NewLine()
out.WriteString("SELECT") out.WriteString("SELECT")
s.OptimizerHints.Serialize(statementType, out, options...)
if s.Distinct { if s.Distinct {
out.WriteString("DISTINCT") out.WriteString("DISTINCT")
@ -286,12 +311,16 @@ func (s *ClauseSetStmtOperator) Serialize(statementType StatementType, out *SQLB
// ClauseUpdate struct // ClauseUpdate struct
type ClauseUpdate struct { type ClauseUpdate struct {
Table SerializerTable Table SerializerTable
// MySQL only
OptimizerHints optimizerHints
} }
// Serialize serializes clause into SQLBuilder // Serialize serializes clause into SQLBuilder
func (u *ClauseUpdate) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) { func (u *ClauseUpdate) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
out.NewLine() out.NewLine()
out.WriteString("UPDATE") out.WriteString("UPDATE")
u.OptimizerHints.Serialize(statementType, out, options...)
if utils.IsNil(u.Table) { if utils.IsNil(u.Table) {
panic("jet: table to update is nil") panic("jet: table to update is nil")
@ -342,6 +371,9 @@ func (s *SetClause) Serialize(statementType StatementType, out *SQLBuilder, opti
type ClauseInsert struct { type ClauseInsert struct {
Table SerializerTable Table SerializerTable
Columns []Column Columns []Column
// MySQL only
OptimizerHints optimizerHints
} }
// GetColumns gets list of columns for insert // GetColumns gets list of columns for insert
@ -355,13 +387,15 @@ func (i *ClauseInsert) GetColumns() []Column {
// Serialize serializes clause into SQLBuilder // Serialize serializes clause into SQLBuilder
func (i *ClauseInsert) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) { func (i *ClauseInsert) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
out.NewLine()
out.WriteString("INSERT INTO")
if utils.IsNil(i.Table) { if utils.IsNil(i.Table) {
panic("jet: table is nil for INSERT clause") panic("jet: table is nil for INSERT clause")
} }
out.NewLine()
out.WriteString("INSERT")
i.OptimizerHints.Serialize(statementType, out, options...)
out.WriteString("INTO")
i.Table.serialize(statementType, out) i.Table.serialize(statementType, out)
if len(i.Columns) > 0 { if len(i.Columns) > 0 {
@ -449,17 +483,17 @@ func (v *ClauseQuery) Serialize(statementType StatementType, out *SQLBuilder, op
// ClauseDelete struct // ClauseDelete struct
type ClauseDelete struct { type ClauseDelete struct {
Table SerializerTable Table SerializerTable
// MySQL only
OptimizerHints optimizerHints
} }
// Serialize serializes clause into SQLBuilder // Serialize serializes clause into SQLBuilder
func (d *ClauseDelete) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) { func (d *ClauseDelete) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
out.NewLine() out.NewLine()
out.WriteString("DELETE FROM") out.WriteString("DELETE")
d.OptimizerHints.Serialize(statementType, out, options...)
if d.Table == nil { out.WriteString("FROM")
panic("jet: nil table in DELETE clause")
}
d.Table.serialize(statementType, out, FallTrough(options)...) d.Table.serialize(statementType, out, FallTrough(options)...)
} }

View file

@ -6,6 +6,8 @@ import "github.com/go-jet/jet/v2/internal/jet"
type DeleteStatement interface { type DeleteStatement interface {
Statement Statement
OPTIMIZER_HINTS(hints ...OptimizerHint) DeleteStatement
USING(tables ...ReadableTable) DeleteStatement USING(tables ...ReadableTable) DeleteStatement
WHERE(expression BoolExpression) DeleteStatement WHERE(expression BoolExpression) DeleteStatement
ORDER_BY(orderByClauses ...OrderByClause) DeleteStatement ORDER_BY(orderByClauses ...OrderByClause) DeleteStatement
@ -15,7 +17,7 @@ type DeleteStatement interface {
type deleteStatementImpl struct { type deleteStatementImpl struct {
jet.SerializerStatement jet.SerializerStatement
Delete jet.ClauseStatementBegin Delete jet.ClauseDelete
Using jet.ClauseFrom Using jet.ClauseFrom
Where jet.ClauseWhere Where jet.ClauseWhere
OrderBy jet.ClauseOrderBy OrderBy jet.ClauseOrderBy
@ -29,17 +31,22 @@ func newDeleteStatement(table Table) DeleteStatement {
&newDelete.Using, &newDelete.Using,
&newDelete.Where, &newDelete.Where,
&newDelete.OrderBy, &newDelete.OrderBy,
&newDelete.Limit) &newDelete.Limit,
)
newDelete.Delete.Name = "DELETE FROM" newDelete.Delete.Table = table
newDelete.Using.Name = "USING" newDelete.Using.Name = "USING"
newDelete.Delete.Tables = append(newDelete.Delete.Tables, table)
newDelete.Where.Mandatory = true newDelete.Where.Mandatory = true
newDelete.Limit.Count = -1 newDelete.Limit.Count = -1
return newDelete return newDelete
} }
func (d *deleteStatementImpl) OPTIMIZER_HINTS(hints ...OptimizerHint) DeleteStatement {
d.Delete.OptimizerHints = hints
return d
}
func (d *deleteStatementImpl) USING(tables ...ReadableTable) DeleteStatement { func (d *deleteStatementImpl) USING(tables ...ReadableTable) DeleteStatement {
d.Using.Tables = readableTablesToSerializerList(tables) d.Using.Tables = readableTablesToSerializerList(tables)
return d return d

View file

@ -6,6 +6,8 @@ import "github.com/go-jet/jet/v2/internal/jet"
type InsertStatement interface { type InsertStatement interface {
Statement Statement
OPTIMIZER_HINTS(hints ...OptimizerHint) InsertStatement
// Insert row of values // Insert row of values
VALUES(value interface{}, values ...interface{}) InsertStatement VALUES(value interface{}, values ...interface{}) InsertStatement
// Insert row of values, where value for each column is extracted from filed of structure data. // Insert row of values, where value for each column is extracted from filed of structure data.
@ -22,7 +24,10 @@ type InsertStatement interface {
func newInsertStatement(table Table, columns []jet.Column) InsertStatement { func newInsertStatement(table Table, columns []jet.Column) InsertStatement {
newInsert := &insertStatementImpl{} newInsert := &insertStatementImpl{}
newInsert.SerializerStatement = jet.NewStatementImpl(Dialect, jet.InsertStatementType, newInsert, newInsert.SerializerStatement = jet.NewStatementImpl(Dialect, jet.InsertStatementType, newInsert,
&newInsert.Insert, &newInsert.ValuesQuery, &newInsert.OnDuplicateKey) &newInsert.Insert,
&newInsert.ValuesQuery,
&newInsert.OnDuplicateKey,
)
newInsert.Insert.Table = table newInsert.Insert.Table = table
newInsert.Insert.Columns = columns newInsert.Insert.Columns = columns
@ -38,6 +43,11 @@ type insertStatementImpl struct {
OnDuplicateKey onDuplicateKeyUpdateClause OnDuplicateKey onDuplicateKeyUpdateClause
} }
func (is *insertStatementImpl) OPTIMIZER_HINTS(hints ...OptimizerHint) InsertStatement {
is.Insert.OptimizerHints = hints
return is
}
func (is *insertStatementImpl) VALUES(value interface{}, values ...interface{}) InsertStatement { func (is *insertStatementImpl) VALUES(value interface{}, values ...interface{}) InsertStatement {
is.ValuesQuery.Rows = append(is.ValuesQuery.Rows, jet.UnwindRowFromValues(value, values)) is.ValuesQuery.Rows = append(is.ValuesQuery.Rows, jet.UnwindRowFromValues(value, values))
return is return is

19
mysql/optimizer_hints.go Normal file
View file

@ -0,0 +1,19 @@
package mysql
import (
"fmt"
"github.com/go-jet/jet/v2/internal/jet"
)
// OptimizerHint provides a way to optimize query execution per-statement basis
type OptimizerHint = jet.OptimizerHint
// MAX_EXECUTION_TIME limits statement execution time
func MAX_EXECUTION_TIME(miliseconds int) OptimizerHint {
return OptimizerHint(fmt.Sprintf("MAX_EXECUTION_TIME(%d)", miliseconds))
}
// QB_NAME assigns name to query block
func QB_NAME(name string) OptimizerHint {
return OptimizerHint(fmt.Sprintf("QB_NAME(%s)", name))
}

View file

@ -40,6 +40,8 @@ type SelectStatement interface {
jet.HasProjections jet.HasProjections
Expression Expression
OPTIMIZER_HINTS(hints ...OptimizerHint) SelectStatement
DISTINCT() SelectStatement DISTINCT() SelectStatement
FROM(tables ...ReadableTable) SelectStatement FROM(tables ...ReadableTable) SelectStatement
WHERE(expression BoolExpression) SelectStatement WHERE(expression BoolExpression) SelectStatement
@ -65,9 +67,19 @@ func SELECT(projection Projection, projections ...Projection) SelectStatement {
func newSelectStatement(table ReadableTable, projections []Projection) SelectStatement { func newSelectStatement(table ReadableTable, projections []Projection) SelectStatement {
newSelect := &selectStatementImpl{} newSelect := &selectStatementImpl{}
newSelect.ExpressionStatement = jet.NewExpressionStatementImpl(Dialect, jet.SelectStatementType, newSelect, &newSelect.Select, newSelect.ExpressionStatement = jet.NewExpressionStatementImpl(Dialect, jet.SelectStatementType, newSelect,
&newSelect.From, &newSelect.Where, &newSelect.GroupBy, &newSelect.Having, &newSelect.Window, &newSelect.OrderBy, &newSelect.Select,
&newSelect.Limit, &newSelect.Offset, &newSelect.For, &newSelect.ShareLock) &newSelect.From,
&newSelect.Where,
&newSelect.GroupBy,
&newSelect.Having,
&newSelect.Window,
&newSelect.OrderBy,
&newSelect.Limit,
&newSelect.Offset,
&newSelect.For,
&newSelect.ShareLock,
)
newSelect.Select.ProjectionList = projections newSelect.Select.ProjectionList = projections
if table != nil { if table != nil {
@ -100,6 +112,11 @@ type selectStatementImpl struct {
ShareLock jet.ClauseOptional ShareLock jet.ClauseOptional
} }
func (s *selectStatementImpl) OPTIMIZER_HINTS(hints ...OptimizerHint) SelectStatement {
s.Select.OptimizerHints = hints
return s
}
func (s *selectStatementImpl) DISTINCT() SelectStatement { func (s *selectStatementImpl) DISTINCT() SelectStatement {
s.Select.Distinct = true s.Select.Distinct = true
return s return s

View file

@ -6,6 +6,8 @@ import "github.com/go-jet/jet/v2/internal/jet"
type UpdateStatement interface { type UpdateStatement interface {
jet.Statement jet.Statement
OPTIMIZER_HINTS(hints ...OptimizerHint) UpdateStatement
SET(value interface{}, values ...interface{}) UpdateStatement SET(value interface{}, values ...interface{}) UpdateStatement
MODEL(data interface{}) UpdateStatement MODEL(data interface{}) UpdateStatement
@ -36,6 +38,11 @@ func newUpdateStatement(table Table, columns []jet.Column) UpdateStatement {
return update return update
} }
func (u *updateStatementImpl) OPTIMIZER_HINTS(hints ...OptimizerHint) UpdateStatement {
u.Update.OptimizerHints = hints
return u
}
func (u *updateStatementImpl) SET(value interface{}, values ...interface{}) UpdateStatement { func (u *updateStatementImpl) SET(value interface{}, values ...interface{}) UpdateStatement {
columnAssigment, isColumnAssigment := value.(ColumnAssigment) columnAssigment, isColumnAssigment := value.(ColumnAssigment)

View file

@ -14,7 +14,7 @@ type DeleteStatement interface {
type deleteStatementImpl struct { type deleteStatementImpl struct {
jet.SerializerStatement jet.SerializerStatement
Delete jet.ClauseStatementBegin Delete jet.ClauseDelete
Using jet.ClauseFrom Using jet.ClauseFrom
Where jet.ClauseWhere Where jet.ClauseWhere
Returning jet.ClauseReturning Returning jet.ClauseReturning
@ -28,8 +28,7 @@ func newDeleteStatement(table WritableTable) DeleteStatement {
&newDelete.Where, &newDelete.Where,
&newDelete.Returning) &newDelete.Returning)
newDelete.Delete.Name = "DELETE FROM" newDelete.Delete.Table = table
newDelete.Delete.Tables = append(newDelete.Delete.Tables, table)
newDelete.Using.Name = "USING" newDelete.Using.Name = "USING"
newDelete.Where.Mandatory = true newDelete.Where.Mandatory = true

View file

@ -15,7 +15,7 @@ type DeleteStatement interface {
type deleteStatementImpl struct { type deleteStatementImpl struct {
jet.SerializerStatement jet.SerializerStatement
Delete jet.ClauseStatementBegin Delete jet.ClauseDelete
Where jet.ClauseWhere Where jet.ClauseWhere
OrderBy jet.ClauseOrderBy OrderBy jet.ClauseOrderBy
Limit jet.ClauseLimit Limit jet.ClauseLimit
@ -32,8 +32,7 @@ func newDeleteStatement(table Table) DeleteStatement {
&newDelete.Returning, &newDelete.Returning,
) )
newDelete.Delete.Name = "DELETE FROM" newDelete.Delete.Table = table
newDelete.Delete.Tables = append(newDelete.Delete.Tables, table)
newDelete.Where.Mandatory = true newDelete.Where.Mandatory = true
newDelete.Limit.Count = -1 newDelete.Limit.Count = -1

View file

@ -2,6 +2,7 @@ package mysql
import ( import (
"context" "context"
"database/sql"
"github.com/go-jet/jet/v2/internal/testutils" "github.com/go-jet/jet/v2/internal/testutils"
. "github.com/go-jet/jet/v2/mysql" . "github.com/go-jet/jet/v2/mysql"
"github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table" "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table"
@ -98,3 +99,22 @@ WHERE (staff.staff_id != ?) AND (rental.rental_id < ?);
testutils.AssertExecAndRollback(t, stmt, db) testutils.AssertExecAndRollback(t, stmt, db)
} }
func TestDeleteOptimizerHints(t *testing.T) {
stmt := Link.DELETE().
OPTIMIZER_HINTS(QB_NAME("deleteIns"), "MRR(link)").
WHERE(
Link.Name.IN(String("Gmail"), String("Outlook")),
)
testutils.AssertDebugStatementSql(t, stmt, `
DELETE /*+ QB_NAME(deleteIns) MRR(link) */ FROM test_sample.link
WHERE link.name IN ('Gmail', 'Outlook');
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
_, err := stmt.Exec(tx)
require.NoError(t, err)
})
}

View file

@ -370,3 +370,23 @@ func TestInsertWithExecContext(t *testing.T) {
require.Error(t, err, "context deadline exceeded") require.Error(t, err, "context deadline exceeded")
} }
func TestInsertOptimizerHints(t *testing.T) {
stmt := Link.INSERT(Link.MutableColumns).
OPTIMIZER_HINTS(QB_NAME("qbIns"), "NO_ICP(link)").
MODEL(model.Link{
URL: "http://www.google.com",
Name: "Google",
})
testutils.AssertDebugStatementSql(t, stmt, `
INSERT /*+ QB_NAME(qbIns) NO_ICP(link) */ INTO test_sample.link (url, name, description)
VALUES ('http://www.google.com', 'Google', NULL);
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
_, err := stmt.Exec(tx)
require.NoError(t, err)
})
}

View file

@ -1189,3 +1189,25 @@ ORDER BY film.film_id;
] ]
`) `)
} }
func TestSelectOptimizerHints(t *testing.T) {
stmt := SELECT(Actor.AllColumns).
OPTIMIZER_HINTS(MAX_EXECUTION_TIME(1), QB_NAME("mainQueryBlock"), "NO_ICP(actor)").
DISTINCT().
FROM(Actor)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT /*+ MAX_EXECUTION_TIME(1) QB_NAME(mainQueryBlock) NO_ICP(actor) */ DISTINCT actor.actor_id AS "actor.actor_id",
actor.first_name AS "actor.first_name",
actor.last_name AS "actor.last_name",
actor.last_update AS "actor.last_update"
FROM dvds.actor;
`)
var actors []model.Actor
err := stmt.QueryContext(context.Background(), db, &actors)
require.NoError(t, err)
require.Len(t, actors, 200)
}

View file

@ -5,7 +5,7 @@ import (
"database/sql" "database/sql"
"github.com/go-jet/jet/v2/internal/testutils" "github.com/go-jet/jet/v2/internal/testutils"
. "github.com/go-jet/jet/v2/mysql" . "github.com/go-jet/jet/v2/mysql"
"github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table" . "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table"
"github.com/go-jet/jet/v2/tests/.gentestdata/mysql/test_sample/model" "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/test_sample/model"
. "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/test_sample/table" . "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/test_sample/table"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -260,10 +260,10 @@ func TestUpdateExecContext(t *testing.T) {
} }
func TestUpdateWithJoin(t *testing.T) { func TestUpdateWithJoin(t *testing.T) {
statement := table.Staff.INNER_JOIN(table.Address, table.Address.AddressID.EQ(table.Staff.AddressID)). statement := Staff.INNER_JOIN(Address, Address.AddressID.EQ(Staff.AddressID)).
UPDATE(table.Staff.LastName). UPDATE(Staff.LastName).
SET(String("New staff name")). SET(String("New staff name")).
WHERE(table.Staff.StaffID.EQ(Int(1))) WHERE(Staff.StaffID.EQ(Int(1)))
testutils.AssertStatementSql(t, statement, ` testutils.AssertStatementSql(t, statement, `
UPDATE dvds.staff UPDATE dvds.staff
@ -274,3 +274,29 @@ WHERE staff.staff_id = ?;
testutils.AssertExecAndRollback(t, statement, db) testutils.AssertExecAndRollback(t, statement, db)
} }
func TestUpdateOptimizerHints(t *testing.T) {
stmt := Link.UPDATE(Link.AllColumns).
OPTIMIZER_HINTS(QB_NAME("qbInsert"), "MRR(link)").
MODEL(model.Link{
ID: 501,
URL: "http://www.duckduckgo.com",
Name: "DuckDuckGo",
}).
WHERE(Link.Name.EQ(String("Bing")))
testutils.AssertDebugStatementSql(t, stmt, `
UPDATE /*+ QB_NAME(qbInsert) MRR(link) */ test_sample.link
SET id = 501,
url = 'http://www.duckduckgo.com',
name = 'DuckDuckGo',
description = NULL
WHERE link.name = 'Bing';
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
_, err := stmt.Exec(tx)
require.NoError(t, err)
})
}