Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

engine.Reload(): read InnoDB tables sizes including FULLTEXT index volume #17118

Merged
merged 29 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
95a6bf1
TestShowTablesWithSizes: add table with fulltext key
shlomi-noach Oct 29, 2024
dee69a4
complete test
shlomi-noach Oct 29, 2024
452ec2c
adding flavor.baseShowFtsTablesWithSizes()
shlomi-noach Oct 29, 2024
bc6316b
Adding TablenameToFilename reimplementation of the MySQL function
shlomi-noach Oct 29, 2024
24ff9e8
(c *Conn) BaseShowFtsTablesWithSizes()
shlomi-noach Oct 29, 2024
59f0f9f
(dbc *Conn) BaseShowFtsTablesWithSizes()
shlomi-noach Oct 29, 2024
86f2078
minor refactor
shlomi-noach Oct 29, 2024
4ac4dd0
Refactor: this isn't about specifically looking for FTS tables, we no…
shlomi-noach Oct 30, 2024
93ab704
no need to return error
shlomi-noach Oct 30, 2024
f04958c
use BaseShowInnodbTableSize()
shlomi-noach Oct 30, 2024
1aba77b
minor testing refactor
shlomi-noach Oct 30, 2024
4471468
adding test that validates engine.Reload, and specifically validates …
shlomi-noach Oct 30, 2024
46febdc
adapt unit tests
shlomi-noach Oct 30, 2024
d467daf
adapt TestReloadWithSwappedTables
shlomi-noach Oct 30, 2024
80ea72d
adapt TestGetTableForPos
shlomi-noach Oct 30, 2024
2dfba2c
adapt TestHistorian
shlomi-noach Oct 30, 2024
5fc6eea
adapt TestReloadSchema
shlomi-noach Oct 30, 2024
25f4bc5
testing adding, modifying, dropping a view
shlomi-noach Oct 30, 2024
a3a4fe0
solve flakiness: add all possible permutations of query
shlomi-noach Oct 30, 2024
08aec12
innodbTablesStats lazy initialization
shlomi-noach Nov 5, 2024
d4a3e79
Remove TablesWithSize80 query
shlomi-noach Nov 5, 2024
c4066b1
mysql.InnoDBTableSizes instead of mysql.TablesWithSize80
shlomi-noach Nov 5, 2024
c5a0678
optimizing parsing
shlomi-noach Nov 5, 2024
cbe82ba
adapt test
shlomi-noach Nov 5, 2024
40133a6
remove redundant test
shlomi-noach Nov 5, 2024
f634366
TestShowTablesWithSizes: skip test if BaseShowTablesWithSizes is empty
shlomi-noach Nov 7, 2024
c9c2b0c
code comment
shlomi-noach Nov 7, 2024
27608c6
adapt TablesWithSize57 to have non-NULL value for CREATE_TIME
shlomi-noach Nov 7, 2024
25949d4
Support for 'legacy' (MySQL 5.7.31 at this time) env for fakesqldb; a…
shlomi-noach Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
375 changes: 375 additions & 0 deletions go/mysql/collations/charset/filename.go

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions go/mysql/collations/charset/filename_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
Copyright 2024 The Vitess Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package charset

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestTablenameToFilename(t *testing.T) {
testCases := []struct {
tablename string
filename string
}{
{
tablename: "my_table123",
filename: "my_table123",
},
{
tablename: "my-table",
filename: "my@002dtable",
},
{
tablename: "my$table",
filename: "my@0024table",
},
{
tablename: "myát",
filename: "my@0ht",
},
{
tablename: "myÃt",
filename: "my@0jt",
},
{
tablename: "myאt",
filename: "my@05d0t",
},
}

for _, tc := range testCases {
t.Run(tc.tablename, func(t *testing.T) {
filename := TablenameToFilename(tc.tablename)
assert.Equal(t, tc.filename, filename, "original bytes: %x", []byte(tc.tablename))
})
}
}
1 change: 1 addition & 0 deletions go/mysql/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ package config

const DefaultSQLMode = "ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"
const DefaultMySQLVersion = "8.0.30"
const LegacyMySQLVersion = "5.7.31"
7 changes: 6 additions & 1 deletion go/mysql/fakesqldb/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ type ExpectedExecuteFetch struct {

// New creates a server, and starts listening.
func New(t testing.TB) *DB {
return NewWithEnv(t, vtenv.NewTestEnv())
}

// NewWithEnv creates a server, and starts listening.
func NewWithEnv(t testing.TB, env *vtenv.Environment) *DB {
// Pick a path for our socket.
socketDir, err := os.MkdirTemp("", "fakesqldb")
if err != nil {
Expand All @@ -185,7 +190,7 @@ func New(t testing.TB) *DB {
queryPatternUserCallback: make(map[*regexp.Regexp]func(string)),
patternData: make(map[string]exprResult),
lastErrorMu: sync.Mutex{},
env: vtenv.NewTestEnv(),
env: env,
}

db.Handler = db
Expand Down
6 changes: 6 additions & 0 deletions go/mysql/flavor.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ type flavor interface {

baseShowTables() string
baseShowTablesWithSizes() string
baseShowInnodbTableSizes() string

supportsCapability(capability capabilities.FlavorCapability) (bool, error)
}
Expand Down Expand Up @@ -454,6 +455,11 @@ func (c *Conn) BaseShowTablesWithSizes() string {
return c.flavor.baseShowTablesWithSizes()
}

// BaseShowInnodbTableSizes returns a query that shows innodb-internal FULLTEXT index tables and their sizes
func (c *Conn) BaseShowInnodbTableSizes() string {
return c.flavor.baseShowInnodbTableSizes()
}

// SupportsCapability checks if the database server supports the given capability
func (c *Conn) SupportsCapability(capability capabilities.FlavorCapability) (bool, error) {
return c.flavor.supportsCapability(capability)
Expand Down
4 changes: 4 additions & 0 deletions go/mysql/flavor_filepos.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ func (*filePosFlavor) baseShowTablesWithSizes() string {
return TablesWithSize56
}

func (filePosFlavor) baseShowInnodbTableSizes() string {
return ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be:

mysqlFlavor{}.baseShowInnodbTableSizes()

? I think so...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just making sure I understand correctly, did you mean that filePosFlavor. baseShowInnodbTableSizes () should return the new query proposed in this PR, as opposed to an empty string? Why, then does filePosFlavor.baseShowTablesWithSizes() return a TablesWithSize56 result as opposed to TablesWithSize80?

In accordance with the rest of filePosFlavor behavior, I don't think it should be using 8.0-grade queries?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. I don't think file/position is really tied to a version like that. We can leave it like it is.

}

// supportsCapability is part of the Flavor interface.
func (f *filePosFlavor) supportsCapability(capability capabilities.FlavorCapability) (bool, error) {
switch capability {
Expand Down
4 changes: 4 additions & 0 deletions go/mysql/flavor_mariadb_binlog_playback.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ func (mariadbFlavor) baseShowTables() string {
return mysqlFlavor{}.baseShowTables()
}

func (mariadbFlavor) baseShowInnodbTableSizes() string {
return ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this one, I think:

mysqlFlavor{}.baseShowInnodbTableSizes()

}

// baseShowTablesWithSizes is part of the Flavor interface.
func (mariadbFlavor101) baseShowTablesWithSizes() string {
return TablesWithSize56
Expand Down
115 changes: 72 additions & 43 deletions go/mysql/flavor_mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,63 +412,92 @@ const BaseShowTables = `SELECT t.table_name,
t.table_schema = database()
`

// TablesWithSize80 is a query to select table along with size for mysql 8.0
// Note the following:
// - `TABLES`.`TABLE_NAME` has `utf8mb4_0900_ai_ci` collation. `INNODB_TABLESPACES`.`NAME` has `utf8mb3_general_ci`.
// We normalize the collation to get better query performance (we force the casting at the time of our choosing)
// - InnoDB has different table names than MySQL does, in particular for partitioned tables. As far as InnoDB
// is concerned, each partition is its own table.
// - We use a `UNION ALL` approach to handle two distinct scenarios: tables that are partitioned and those that are not.
// Since we `LEFT JOIN` from `TABLES` to `INNODB_TABLESPACES`, we know we already do full table scan on `TABLES`. We therefore
// don't mind spending some extra computation time (as in `CONCAT(t.table_schema, '/', t.table_name, '#p#%') COLLATE utf8mb3_general_ci`)
// to make things easier for the JOIN.
// - We utilize `INFORMATION_SCHEMA`.`TABLES`.`CREATE_OPTIONS` column to tell if the table is partitioned or not. The column
// may be `NULL` or may have multiple attributes, one of which is "partitioned", which we are looking for.
// - In a partitioned table, InnoDB will return multiple rows for the same table name, one for each partition, which we successively SUM.
// We also `SUM` the sizes in the non-partitioned case. This is not because we need to, but because it makes the query
// symmetric and less prone to future edit errors.
const TablesWithSize80 = `SELECT t.table_name,
t.table_type,
UNIX_TIMESTAMP(t.create_time),
t.table_comment,
SUM(i.file_size),
SUM(i.allocated_size)
FROM information_schema.tables t
LEFT JOIN (SELECT name, file_size, allocated_size FROM information_schema.innodb_tablespaces WHERE name LIKE CONCAT(database(), '/%')) i
ON i.name = CONCAT(t.table_schema, '/', t.table_name) COLLATE utf8mb3_general_ci
WHERE
t.table_schema = database() AND IFNULL(t.create_options, '') NOT LIKE '%partitioned%'
GROUP BY
t.table_schema, t.table_name, t.table_type, t.create_time, t.table_comment
UNION ALL
SELECT t.table_name,
t.table_type,
UNIX_TIMESTAMP(t.create_time),
t.table_comment,
SUM(i.file_size),
SUM(i.allocated_size)
FROM information_schema.tables t
LEFT JOIN (SELECT name, file_size, allocated_size FROM information_schema.innodb_tablespaces WHERE name LIKE CONCAT(database(), '/%')) i
ON i.name LIKE (CONCAT(t.table_schema, '/', t.table_name, '#p#%') COLLATE utf8mb3_general_ci)
WHERE
t.table_schema = database() AND t.create_options LIKE '%partitioned%'
GROUP BY
t.table_schema, t.table_name, t.table_type, t.create_time, t.table_comment
// InnoDBTableSizes: a query to return file/allocated sizes for InnoDB tables.
// File sizes and allocated sizes are found in information_schema.innodb_tablespaces
// Table names in information_schema.innodb_tablespaces match those in information_schema.tables, even for table names
// with special characters. This, a innodb_tablespaces.name could be `my-db/my-table`.
// These tablespaces will have one entry for every InnoDB table, hidden or internal. This means:
// - One entry for every partition in a partitioned table.
// - Several entries for any FULLTEXT index (FULLTEXT indexes are not BTREEs and are implemented using multiple hidden tables)
// So a single table wih a FULLTEXT index will have one entry for the "normal" table, plus multiple more entries for
// every FTS index hidden tables.
// Thankfully FULLTEXT does not work with Partitioning so this does not explode too much.
// Next thing is that FULLTEXT hidden table names do not resemble the original table name, and could look like:
// `a-b/fts_000000000000075e_00000000000005f9_index_2`.
// To unlock the identify of this table we turn to information_schema.innodb_tables. These table similarly has one entry for
// every InnoDB table, normal or hidden. It also has a `TABLE_ID` value. Given some table with FULLTEXT keys, its TABLE_ID
// is encoded in the names of the hidden tables in information_schema.innodb_tablespaces: `000000000000075e` in the
// example above.
//
// The query below is a two part:
// 1. Finding the "normal" tables only, those that the user created. We note their file size and allocated size.
// 2. Finding the hidden tables only, those that implement FTS keys. We aggregate their file size and allocated size grouping
// by the original table name with which they're associated.
//
// A table that has a FULLTEXT index will have two entries in the result set:
// - one for the "normal" table size (actual rows, texts, etc.)
// - and one for the aggregated hidden table size
// The code that reads the results of this query will need to add the two.
// Similarly, the code will need to know how to aggregate the sizes of partitioned tables, which could appear as:
// - `mydb/tbl_part#p#p0`
// - `mydb/tbl_part#p#p1`
// - `mydb/tbl_part#p#p2`
// - `mydb/tbl_part#p#p3`
//
// Lastly, we note that table name in information_schema.innodb_tables are encoded. A table that shows as
// `my-db/my-table` in information_schema.innodb_tablespaces will show as `my@002ddb/my@002dtable` in information_schema.innodb_tables.
// So this query returns InnoDB-encoded table names. The golang code reading those will have to decode the names.
const InnoDBTableSizes = `
SELECT
it.name,
its.file_size as normal_tables_sum_file_size,
its.allocated_size as normal_tables_sum_allocated_size
FROM
information_schema.innodb_tables it
JOIN information_schema.innodb_tablespaces its
ON (its.space = it.space)
WHERE
its.name LIKE CONCAT(database(), '/%')
AND its.name NOT LIKE CONCAT(database(), '/fts_%')
UNION ALL
SELECT
it.name,
SUM(its.file_size) as hidden_tables_sum_file_size,
SUM(its.allocated_size) as hidden_tables_sum_allocated_size
FROM
information_schema.innodb_tables it
JOIN information_schema.innodb_tablespaces its
ON (
its.name LIKE CONCAT(database(), '/fts_', CONVERT(LPAD(HEX(table_id), 16, '0') USING utf8mb3) COLLATE utf8mb3_general_ci, '_%')
)
WHERE
its.name LIKE CONCAT(database(), '/fts_%')
GROUP BY it.name
`

// baseShowTablesWithSizes is part of the Flavor interface.
func (mysqlFlavor57) baseShowTablesWithSizes() string {
return TablesWithSize57
}

// baseShowInnodbTableSizes is part of the Flavor interface.
func (mysqlFlavor57) baseShowInnodbTableSizes() string {
return ""
mattlord marked this conversation as resolved.
Show resolved Hide resolved
}

// supportsCapability is part of the Flavor interface.
func (f mysqlFlavor) supportsCapability(capability capabilities.FlavorCapability) (bool, error) {
return capabilities.MySQLVersionHasCapability(f.serverVersion, capability)
}

// baseShowTablesWithSizes is part of the Flavor interface.
func (mysqlFlavor) baseShowTablesWithSizes() string {
return TablesWithSize80
return "" // Won't be used, as InnoDBTableSizes is defined, and schema.Engine will use that, instead.
}

// baseShowInnodbTableSizes is part of the Flavor interface.
func (mysqlFlavor) baseShowInnodbTableSizes() string {
return InnoDBTableSizes
}

func (mysqlFlavor) setReplicationSourceCommand(params *ConnParams, host string, port int32, heartbeatInterval float64, connectRetry int) string {
Expand Down
7 changes: 4 additions & 3 deletions go/mysql/flavor_mysql_legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,13 @@ GROUP BY table_name,
// We join with a subquery that materializes the data from `information_schema.innodb_sys_tablespaces`
// early for performance reasons. This effectively causes only a single read of `information_schema.innodb_sys_tablespaces`
// per query.
// Note that 5.7 has NULL for a VIEW's create_time, so we use IFNULL to make it 1 (non NULL and non zero).
const TablesWithSize57 = `SELECT t.table_name,
t.table_type,
UNIX_TIMESTAMP(t.create_time),
IFNULL(UNIX_TIMESTAMP(t.create_time), 1),
t.table_comment,
IFNULL(SUM(i.file_size), SUM(t.data_length + t.index_length)),
IFNULL(SUM(i.allocated_size), SUM(t.data_length + t.index_length))
IFNULL(SUM(i.file_size), SUM(t.data_length + t.index_length)) AS file_size,
IFNULL(SUM(i.allocated_size), SUM(t.data_length + t.index_length)) AS allocated_size
FROM information_schema.tables t
LEFT OUTER JOIN (
SELECT space, file_size, allocated_size, name
Expand Down
7 changes: 6 additions & 1 deletion go/mysql/flavor_mysqlgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,12 @@ func (mysqlGRFlavor) baseShowTables() string {
}

func (mysqlGRFlavor) baseShowTablesWithSizes() string {
return TablesWithSize80
return "" // Won't be used, as InnoDBTableSizes is defined, and schema.Engine will use that, instead.
}

// baseShowInnodbTableSizes is part of the Flavor interface.
func (mysqlGRFlavor) baseShowInnodbTableSizes() string {
return InnoDBTableSizes
}

// supportsCapability is part of the Flavor interface.
Expand Down
31 changes: 31 additions & 0 deletions go/mysql/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,29 @@ var BaseShowTablesWithSizesFields = append(BaseShowTablesFields, &querypb.Field{
Charset: collations.CollationBinaryID,
Flags: uint32(querypb.MySqlFlag_BINARY_FLAG | querypb.MySqlFlag_NUM_FLAG),
})
var BaseInnoDBTableSizesFields = []*querypb.Field{{
Name: "it.name",
Type: querypb.Type_VARCHAR,
Table: "tables",
OrgTable: "TABLES",
Database: "information_schema",
OrgName: "TABLE_NAME",
ColumnLength: 192,
Charset: uint32(collations.SystemCollation.Collation),
Flags: uint32(querypb.MySqlFlag_NOT_NULL_FLAG),
}, {
Name: "i.file_size",
Type: querypb.Type_INT64,
ColumnLength: 11,
Charset: collations.CollationBinaryID,
Flags: uint32(querypb.MySqlFlag_BINARY_FLAG | querypb.MySqlFlag_NUM_FLAG),
}, {
Name: "i.allocated_size",
Type: querypb.Type_INT64,
ColumnLength: 11,
Charset: collations.CollationBinaryID,
Flags: uint32(querypb.MySqlFlag_BINARY_FLAG | querypb.MySqlFlag_NUM_FLAG),
}}

// BaseShowTablesRow returns the fields from a BaseShowTables or
// BaseShowTablesForTable command.
Expand All @@ -116,6 +139,14 @@ func BaseShowTablesWithSizesRow(tableName string, isView bool, comment string) [
)
}

func BaseInnoDBTableSizesRow(dbName string, tableName string) []sqltypes.Value {
return []sqltypes.Value{
sqltypes.MakeTrusted(sqltypes.VarChar, []byte(dbName+"/"+tableName)),
sqltypes.MakeTrusted(sqltypes.Int64, []byte("100")), // file_size
sqltypes.MakeTrusted(sqltypes.Int64, []byte("150")), // allocated_size
}
}

// ShowPrimaryFields contains the fields for a BaseShowPrimary.
var ShowPrimaryFields = []*querypb.Field{{
Name: "table_name",
Expand Down
6 changes: 6 additions & 0 deletions go/vt/vtenv/vtenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ func NewTestEnv() *Environment {
}
}

func NewLegacyTestEnv() *Environment {
env := NewTestEnv()
env.mysqlVersion = config.LegacyMySQLVersion
return env
}

func (e *Environment) CollationEnv() *collations.Environment {
return e.collationEnv
}
Expand Down
7 changes: 4 additions & 3 deletions go/vt/vtexplain/vtexplain_vttablet.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ func newTabletEnvironment(ddls []sqlparser.DDLStatement, opts *Options, collatio

showTableRows := make([][]sqltypes.Value, 0, len(ddls))
showTableWithSizesRows := make([][]sqltypes.Value, 0, len(ddls))
innodbTableSizesRows := make([][]sqltypes.Value, 0, len(ddls))

for _, ddl := range ddls {
table := ddl.GetTable().Name.String()
Expand All @@ -455,9 +456,9 @@ func newTabletEnvironment(ddls []sqlparser.DDLStatement, opts *Options, collatio
Fields: mysql.BaseShowTablesWithSizesFields,
Rows: showTableWithSizesRows,
})
tEnv.addResult(mysql.TablesWithSize80, &sqltypes.Result{
Fields: mysql.BaseShowTablesWithSizesFields,
Rows: showTableWithSizesRows,
tEnv.addResult(mysql.InnoDBTableSizes, &sqltypes.Result{
Fields: mysql.BaseInnoDBTableSizesFields,
Rows: innodbTableSizesRows,
})

indexRows := make([][]sqltypes.Value, 0, 4)
Expand Down
Loading
Loading