From 8f3770bcd83527e3d83d09c2dc55af066cefac16 Mon Sep 17 00:00:00 2001 From: deo002 Date: Tue, 9 Jun 2026 23:52:39 +0530 Subject: [PATCH 1/7] MDEV-38992 Add parser support and syntax validation for TABLESAMPLE clause 1. Inspired from the implementation of the limit clause, added support for TABLESAMPLE clause in queries, stored procedures and prepared statements. 2. Added syntax validation for sampling percentage, correct usage with tables(system tables or derived table cannot be sampled). 3. Added tests validating the implementation of the above two. --- mysql-test/main/tablesample.result | 79 ++++++++++++++++++ mysql-test/main/tablesample.test | 126 +++++++++++++++++++++++++++++ sql/item.cc | 16 ++++ sql/item.h | 26 +++++- sql/lex.h | 2 + sql/share/errmsg-utf8.txt | 2 + sql/sp_head.cc | 4 + sql/sql_base.cc | 11 +++ sql/sql_lex.cc | 64 +++++++++++++-- sql/sql_lex.h | 60 ++++++++++++++ sql/sql_prepare.cc | 10 +++ sql/sql_string.cc | 12 +++ sql/sql_string.h | 2 + sql/sql_tablesample.h | 59 ++++++++++++++ sql/sql_type.h | 9 ++- sql/sql_yacc.yy | 54 ++++++++++++- sql/table.h | 3 + 17 files changed, 525 insertions(+), 14 deletions(-) create mode 100644 mysql-test/main/tablesample.result create mode 100644 mysql-test/main/tablesample.test create mode 100644 sql/sql_tablesample.h diff --git a/mysql-test/main/tablesample.result b/mysql-test/main/tablesample.result new file mode 100644 index 0000000000000..8473b23f78264 --- /dev/null +++ b/mysql-test/main/tablesample.result @@ -0,0 +1,79 @@ +DROP TABLE IF EXISTS t1, t2; +DROP VIEW IF EXISTS v1; +CREATE TABLE t1 (a INT, b INT, KEY idx_b(b)); +CREATE TABLE t2 (a INT, c INT); +CREATE VIEW v1 AS SELECT * FROM t1; +SELECT * FROM t1 TABLESAMPLE SYSTEM (10); +a b +SELECT * FROM t1 TABLESAMPLE BERNOULLI (50); +a b +SELECT * FROM t1 TABLESAMPLE SYSTEM (12.5); +a b +SELECT * FROM t1 TABLESAMPLE BERNOULLI (0.5); +a b +SELECT * FROM t1 TABLESAMPLE BERNOULLI (NULL); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'NULL)' at line 1 +SELECT * FROM t1 TABLESAMPLE NONEXISTENTMETHOD (12.5); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'NONEXISTENTMETHOD (12.5)' at line 1 +SELECT * FROM t1 TABLESAMPLE SYSTEM (-12.5); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '-12.5)' at line 1 +SELECT * FROM t1 TABLESAMPLE SYSTEM (10) JOIN t2 ON t1.a = t2.a; +a b a c +SELECT * FROM v1 TABLESAMPLE SYSTEM (20); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +PREPARE stmt2 FROM 'SELECT * FROM t1 TABLESAMPLE BERNOULLI (?)'; +SET @pct = 30; +EXECUTE stmt2 USING @pct; +a b +SET @pct = 300; +EXECUTE stmt2 USING @pct; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +DEALLOCATE PREPARE stmt2; +DROP PROCEDURE IF EXISTS p1; +Warnings: +Note 1305 PROCEDURE test.p1 does not exist +CREATE PROCEDURE p1(IN sample_pct INT) +BEGIN +SELECT * FROM t1 TABLESAMPLE SYSTEM (sample_pct); +END// +CALL p1(40); +a b +CALL p1(101); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +DROP PROCEDURE p1; +CREATE PROCEDURE p1(IN sample_pct DECIMAL) +BEGIN +SELECT * FROM t1 TABLESAMPLE SYSTEM (sample_pct); +END// +CALL p1(40.11); +a b +Warnings: +Note 1265 Data truncated for column 'sample_pct' at row 0 +DROP PROCEDURE p1; +DROP PROCEDURE IF EXISTS p_char; +Warnings: +Note 1305 PROCEDURE test.p_char does not exist +CREATE PROCEDURE p_char(IN sample_pct CHAR(10)) +BEGIN +SELECT * FROM t1 TABLESAMPLE SYSTEM (sample_pct); +END// +ERROR HY000: A variable of a non-numeric based type in TABLESAMPLE clause +DROP PROCEDURE IF EXISTS p_date; +Warnings: +Note 1305 PROCEDURE test.p_date does not exist +CREATE PROCEDURE p_date(IN sample_pct DATE) +BEGIN +SELECT * FROM t1 TABLESAMPLE BERNOULLI (sample_pct); +END// +ERROR HY000: A variable of a non-numeric based type in TABLESAMPLE clause +SELECT * FROM information_schema.tables TABLESAMPLE BERNOULLI (5); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +SELECT * FROM mysql.user TABLESAMPLE SYSTEM (10); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +SELECT * FROM (SELECT * FROM t1) AS derived_tbl TABLESAMPLE SYSTEM (50); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'TABLESAMPLE SYSTEM (50)' at line 1 +WITH cte_tbl AS (SELECT * FROM t1) +SELECT * FROM cte_tbl TABLESAMPLE BERNOULLI (10); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +DROP TABLE t1, t2; +DROP VIEW v1; diff --git a/mysql-test/main/tablesample.test b/mysql-test/main/tablesample.test new file mode 100644 index 0000000000000..460e681053530 --- /dev/null +++ b/mysql-test/main/tablesample.test @@ -0,0 +1,126 @@ +# +# MDEV-38992 SQL Standard TABLESAMPLE clause +# +# For now, there are tests just for checking syntax +# + +--disable_warnings +DROP TABLE IF EXISTS t1, t2; +DROP VIEW IF EXISTS v1; +--enable_warnings + +CREATE TABLE t1 (a INT, b INT, KEY idx_b(b)); +CREATE TABLE t2 (a INT, c INT); +CREATE VIEW v1 AS SELECT * FROM t1; + +# +# Basic Syntax Validation +# +SELECT * FROM t1 TABLESAMPLE SYSTEM (10); +SELECT * FROM t1 TABLESAMPLE BERNOULLI (50); + +SELECT * FROM t1 TABLESAMPLE SYSTEM (12.5); +SELECT * FROM t1 TABLESAMPLE BERNOULLI (0.5); + +--error ER_PARSE_ERROR +SELECT * FROM t1 TABLESAMPLE BERNOULLI (NULL); + +--error ER_PARSE_ERROR +SELECT * FROM t1 TABLESAMPLE NONEXISTENTMETHOD (12.5); + +--error ER_PARSE_ERROR +SELECT * FROM t1 TABLESAMPLE SYSTEM (-12.5); + +SELECT * FROM t1 TABLESAMPLE SYSTEM (10) JOIN t2 ON t1.a = t2.a; + +--error ER_SYNTAX_ERROR +SELECT * FROM v1 TABLESAMPLE SYSTEM (20); + +# +# Testing Prepared statements +# +PREPARE stmt2 FROM 'SELECT * FROM t1 TABLESAMPLE BERNOULLI (?)'; + +SET @pct = 30; +EXECUTE stmt2 USING @pct; + +SET @pct = 300; +--error ER_SYNTAX_ERROR +EXECUTE stmt2 USING @pct; + +DEALLOCATE PREPARE stmt2; + +# +# Testing stored procedures +# +DROP PROCEDURE IF EXISTS p1; + +DELIMITER //; +CREATE PROCEDURE p1(IN sample_pct INT) +BEGIN + SELECT * FROM t1 TABLESAMPLE SYSTEM (sample_pct); +END// +DELIMITER ;// + +CALL p1(40); + +--error ER_SYNTAX_ERROR +CALL p1(101); + +DROP PROCEDURE p1; + +DELIMITER //; +CREATE PROCEDURE p1(IN sample_pct DECIMAL) +BEGIN + SELECT * FROM t1 TABLESAMPLE SYSTEM (sample_pct); +END// +DELIMITER ;// + +CALL p1(40.11); + +DROP PROCEDURE p1; + +DROP PROCEDURE IF EXISTS p_char; + +DELIMITER //; +--error ER_WRONG_SPVAR_TYPE_IN_TABLESAMPLE +CREATE PROCEDURE p_char(IN sample_pct CHAR(10)) +BEGIN + SELECT * FROM t1 TABLESAMPLE SYSTEM (sample_pct); +END// +DELIMITER ;// + +DROP PROCEDURE IF EXISTS p_date; + +DELIMITER //; +--error ER_WRONG_SPVAR_TYPE_IN_TABLESAMPLE +CREATE PROCEDURE p_date(IN sample_pct DATE) +BEGIN + SELECT * FROM t1 TABLESAMPLE BERNOULLI (sample_pct); +END// +DELIMITER ;// + +# +# TABLESAMPLE should not work on system tables +# +--error ER_SYNTAX_ERROR +SELECT * FROM information_schema.tables TABLESAMPLE BERNOULLI (5); + +--error ER_SYNTAX_ERROR +SELECT * FROM mysql.user TABLESAMPLE SYSTEM (10); + +# +# TABLESAMPLE should not work on derived tables +# +--error ER_PARSE_ERROR +SELECT * FROM (SELECT * FROM t1) AS derived_tbl TABLESAMPLE SYSTEM (50); + +--error ER_SYNTAX_ERROR +WITH cte_tbl AS (SELECT * FROM t1) +SELECT * FROM cte_tbl TABLESAMPLE BERNOULLI (10); + +# +# Cleanup +# +DROP TABLE t1, t2; +DROP VIEW v1; \ No newline at end of file diff --git a/sql/item.cc b/sql/item.cc index b5fbab9f6aeda..f7bb1abda33e1 100644 --- a/sql/item.cc +++ b/sql/item.cc @@ -4708,6 +4708,22 @@ bool Item_param::set_from_item(THD *thd, Item *item) DBUG_RETURN(set_limit_clause_param(val)); } } + if (tablesample_clause_param) + { + double val= item->val_real(); + if (item->null_value) + { + set_null(DTCollation_numeric()); + set_handler(&type_handler_null); + DBUG_RETURN(false); + } + else + { + unsigned_flag= item->unsigned_flag; + set_handler(item->type_handler()); + DBUG_RETURN(set_tablesample_clause_param(val)); + } + } st_value tmp; item->save_in_value(thd, &tmp); DBUG_RETURN(set_from_value(thd, tmp, item->type_handler(), *item)); diff --git a/sql/item.h b/sql/item.h index dabe4ca1c8801..d5c5dd375138c 100644 --- a/sql/item.h +++ b/sql/item.h @@ -526,10 +526,11 @@ class Rewritable_query_parameter uint len_in_query; bool limit_clause_param; + bool tablesample_clause_param; Rewritable_query_parameter(uint pos_in_q= 0, uint len_in_q= 0) : pos_in_query(pos_in_q), len_in_query(len_in_q), - limit_clause_param(false) + limit_clause_param(false), tablesample_clause_param(false) { } virtual ~Rewritable_query_parameter() = default; @@ -3405,7 +3406,7 @@ class Item_splocal :public Item_sp_variable, Field *create_field_for_create_select(MEM_ROOT *root, TABLE *table) override { return create_table_field_from_handler(root, table); } - bool is_valid_limit_clause_variable_with_error() const + bool is_valid_numeric_clause_variable_with_error() const { /* In case if the variable has an anchored data type, e.g.: @@ -3413,12 +3414,20 @@ class Item_splocal :public Item_sp_variable, type_handler() is set to &type_handler_null and this function detects such variable as not valid in LIMIT. */ - if (type_handler()->is_limit_clause_valid_type()) + if (type_handler()->is_numeric_clause_valid_type()) return true; my_error(ER_WRONG_SPVAR_TYPE_IN_LIMIT, MYF(0)); return false; } + bool is_valid_tablesample_clause_variable_with_error() const + { + if (type_handler()->is_tablesample_clause_valid_type()) + return true; + my_error(ER_WRONG_SPVAR_TYPE_IN_TABLESAMPLE, MYF(0)); + return false; + } + protected: Item *shallow_copy(THD *thd) const override { return get_item_copy(thd, this); } @@ -4746,6 +4755,12 @@ class Item_param final :public Item_basic_value, set_int(nr, MY_INT64_NUM_DECIMAL_DIGITS); return !unsigned_flag && value.integer < 0; } + bool set_tablesample_clause_param(double d) + { + value.set_handler(&type_handler_double); + set_double(d); + return !unsigned_flag && value.real < 0; + } const String *query_val_str(THD *thd, String *str) const; bool convert_str_value(THD *thd); @@ -4780,6 +4795,11 @@ class Item_param final :public Item_basic_value, return state == SHORT_DATA_VALUE && value.type_handler()->cmp_type() == INT_RESULT; } + bool has_double_value() const + { + return state == SHORT_DATA_VALUE && + value.type_handler()->cmp_type() == REAL_RESULT; + } bool is_stored_routine_parameter() const override { return true; } /* This method is used to make a copy of a basic constant item when diff --git a/sql/lex.h b/sql/lex.h index 61774df019f0d..a2f73eb6b0961 100644 --- a/sql/lex.h +++ b/sql/lex.h @@ -86,6 +86,7 @@ SYMBOL symbols[] = { { "BACKUP", SYM(BACKUP_SYM)}, { "BEFORE", SYM(BEFORE_SYM)}, { "BEGIN", SYM(BEGIN_MARIADB_SYM)}, + { "BERNOULLI", SYM(BERNOULLI)}, { "BETWEEN", SYM(BETWEEN_SYM)}, { "BIGINT", SYM(BIGINT)}, { "BINARY", SYM(BINARY)}, @@ -663,6 +664,7 @@ SYMBOL symbols[] = { { "TABLE", SYM(TABLE_SYM)}, { "TABLE_NAME", SYM(TABLE_NAME_SYM)}, { "TABLES", SYM(TABLES)}, + { "TABLESAMPLE", SYM(TABLESAMPLE_SYM)}, { "TABLESPACE", SYM(TABLESPACE)}, { "TABLE_CHECKSUM", SYM(TABLE_CHECKSUM_SYM)}, { "TEMPORARY", SYM(TEMPORARY)}, diff --git a/sql/share/errmsg-utf8.txt b/sql/share/errmsg-utf8.txt index d6436255da7d1..776df2a73477d 100644 --- a/sql/share/errmsg-utf8.txt +++ b/sql/share/errmsg-utf8.txt @@ -12418,3 +12418,5 @@ ER_JSON_OPTIMIZER_REPLAY_CONTEXT_PARSE_FAILED eng "Failed to parse saved optimizer context: %s at offset %d." ER_JSON_OPTIMIZER_REPLAY_CONTEXT_MATCH_FAILED eng "Failed to match the stats from replay context with the optimizer stats: %s doesn't exist in %s" +ER_WRONG_SPVAR_TYPE_IN_TABLESAMPLE + eng "A variable of a non-numeric based type in TABLESAMPLE clause" diff --git a/sql/sp_head.cc b/sql/sp_head.cc index 1c2b199777351..8e95a781a03bf 100644 --- a/sql/sp_head.cc +++ b/sql/sp_head.cc @@ -121,6 +121,8 @@ bool Item_splocal::append_for_log(THD *thd, String *str) if (limit_clause_param) return str->append_ulonglong(val_uint()); + if (tablesample_clause_param) + return str->append_double(val_real()); /* ROW variables are currently not allowed in select_list, e.g.: @@ -159,6 +161,8 @@ bool Item_splocal_row_field::append_for_log(THD *thd, String *str) if (limit_clause_param) return str->append_ulonglong(val_uint()); + if (tablesample_clause_param) + return str->append_double(val_real()); if (str->append(STRING_WITH_LEN(" NAME_CONST('")) || str->append(&m_name) || diff --git a/sql/sql_base.cc b/sql/sql_base.cc index b451b9bd6e9e2..6a66f94db1892 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -69,6 +69,7 @@ #include "wsrep_trans_observer.h" #endif /* WITH_WSREP */ #include "opt_hints.h" +#include "sql_tablesample.h" bool No_such_table_error_handler::handle_condition(THD *, @@ -8651,6 +8652,16 @@ bool setup_tables(THD *thd, Name_resolution_context *context, } DBUG_ASSERT(item == table_list->jtbm_subselect->optimizer); } + + if (table_list->tablesample_clause) { + if (table_list->is_view_or_derived() || + get_table_category(table_list->db, table_list->table_name) != TABLE_CATEGORY_USER || + table_list->tablesample_clause->fix_tablesample_fields(thd)) + { + my_error(ER_SYNTAX_ERROR, MYF(0)); + DBUG_RETURN(1); + } + } } /* Precompute and store the row types of NATURAL/USING joins. */ diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index 6e1c6df4715b7..7cec455879e26 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -9575,7 +9575,7 @@ bool LEX::mark_item_ident_for_ora_join(THD *thd, Item *item) } -Item *LEX::create_item_limit(THD *thd, const Lex_ident_cli_st *ca) +Item_splocal *LEX::create_item(THD *thd, const Lex_ident_cli_st *ca) { DBUG_ASSERT(thd->m_parser_state->m_lip.get_buf() <= ca->pos()); DBUG_ASSERT(ca->pos() <= ca->end()); @@ -9605,15 +9605,36 @@ Item *LEX::create_item_limit(THD *thd, const Lex_ident_cli_st *ca) #endif safe_to_cache_query= 0; - if (!item->is_valid_limit_clause_variable_with_error()) + return item; +} + +Item *LEX::create_item_limit(THD *thd, const Lex_ident_cli_st *ca) +{ + Item_splocal *num_item = create_item(thd, ca); + if (!num_item) + return NULL; + + if (!num_item->is_valid_numeric_clause_variable_with_error()) return NULL; - item->limit_clause_param= true; - return item; + num_item->limit_clause_param= true; + return num_item; } +Item *LEX::create_item_tablesample(THD *thd, const Lex_ident_cli_st *ca) +{ + Item_splocal *num_item = create_item(thd, ca); + if (!num_item) + return NULL; + + if (!num_item->is_valid_tablesample_clause_variable_with_error()) + return NULL; -Item *LEX::create_item_limit(THD *thd, + num_item->tablesample_clause_param= true; + return num_item; +} + +Item_splocal *LEX::create_item(THD *thd, const Lex_ident_cli_st *ca, const Lex_ident_cli_st *cb) { @@ -9637,12 +9658,39 @@ Item *LEX::create_item_limit(THD *thd, if (unlikely(!(item= create_item_spvar_row_field(thd, rh, &sa, &sb, spv, ca->pos(), cb->end())))) return NULL; - if (!item->is_valid_limit_clause_variable_with_error()) - return NULL; - item->limit_clause_param= true; + return item; } +Item *LEX::create_item_limit(THD *thd, + const Lex_ident_cli_st *ca, + const Lex_ident_cli_st *cb) +{ + Item_splocal *num_item = create_item(thd, ca, cb); + if (!num_item) + return NULL; + + if (!num_item->is_valid_numeric_clause_variable_with_error()) + return NULL; + + num_item->limit_clause_param= true; + return num_item; +} + +Item *LEX::create_item_tablesample(THD *thd, + const Lex_ident_cli_st *ca, + const Lex_ident_cli_st *cb) +{ + Item_splocal *num_item = create_item(thd, ca, cb); + if (!num_item) + return NULL; + + if (!num_item->is_valid_tablesample_clause_variable_with_error()) + return NULL; + + num_item->tablesample_clause_param= true; + return num_item; +} bool LEX::set_user_variable(THD *thd, const LEX_CSTRING *name, Item *val) { diff --git a/sql/sql_lex.h b/sql/sql_lex.h index 5e0258ee42bf3..6ebf18ba28279 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -2092,6 +2092,12 @@ class Query_tables_list */ BINLOG_STMT_UNSAFE_SKIP_LOCKED, + /** + SELECT..TABLESAMPLE is unsafe because the set of rows returned cannot + be predicted. + */ + BINLOG_STMT_UNSAFE_TABLESAMPLE, + /* The last element of this enumeration type. */ BINLOG_STMT_UNSAFE_COUNT }; @@ -4495,6 +4501,34 @@ struct LEX: public Query_tables_list Longlong_hybrid value, ulonglong round, bool is_used); +private: + /* + Create an item for a name in numeric(LIMIT or TABLESAMPLE) clauses: + @param THD - THD, for mem_root + @param var_name - the variable name + @retval - a new Item corresponding to the SP variable, + or NULL on error + (non in SP, unknown variable, wrong data type). + */ + Item_splocal *create_item(THD *thd, const Lex_ident_cli_st *var_name); + + /* + Create an item for a qualified name in numeric(LIMIT or TABLESAMPLE) clause: + @param THD - THD, for mem_root + @param var_name - the variable name + @param field_name - the variable field name + @param start - start in the query (for binary log) + @param end - end in the query (for binary log) + @retval - a new Item corresponding to the SP variable, + or NULL on error + (non in SP, unknown variable, unknown ROW field, + wrong data type). + */ + Item_splocal *create_item(THD *thd, + const Lex_ident_cli_st *var_name, + const Lex_ident_cli_st *field_name); + +public: /* Create an item for a name in LIMIT clause: LIMIT var @param THD - THD, for mem_root @@ -4521,6 +4555,32 @@ struct LEX: public Query_tables_list const Lex_ident_cli_st *var_name, const Lex_ident_cli_st *field_name); + /* + Create an item for a name in TABLESAMPLE clause: SYSTEM(var) or BERNOULLI(var) + @param THD - THD, for mem_root + @param var_name - the variable name + @retval - a new Item corresponding to the SP variable, + or NULL on error + (non in SP, unknown variable, wrong data type). + */ + Item *create_item_tablesample(THD *thd, const Lex_ident_cli_st *var_name); + + /* + Create an item for a qualified name in TABLESAMPLE clause: SYSTEM(var.field) or BERNOULLI(var.field) + @param THD - THD, for mem_root + @param var_name - the variable name + @param field_name - the variable field name + @param start - start in the query (for binary log) + @param end - end in the query (for binary log) + @retval - a new Item corresponding to the SP variable, + or NULL on error + (non in SP, unknown variable, unknown ROW field, + wrong data type). + */ + Item *create_item_tablesample(THD *thd, + const Lex_ident_cli_st *var_name, + const Lex_ident_cli_st *field_name); + Item *create_item_query_expression(THD *thd, st_select_lex_unit *unit); Item *make_item_func_sysdate(THD *thd, uint fsp); diff --git a/sql/sql_prepare.cc b/sql/sql_prepare.cc index 6734c6ad911b8..ca503687e16a2 100644 --- a/sql/sql_prepare.cc +++ b/sql/sql_prepare.cc @@ -294,6 +294,11 @@ class Prepared_statement: public Statement if (param->set_limit_clause_param(param->val_int())) DBUG_RETURN(true); } + if (param->tablesample_clause_param && !param->has_double_value()) + { + if (param->set_tablesample_clause_param(param->val_real())) + DBUG_RETURN(true); + } } DBUG_RETURN(false); } @@ -936,6 +941,11 @@ static bool insert_params_with_log(Prepared_statement *stmt, uchar *null_array, if (param->set_limit_clause_param(param->val_int())) DBUG_RETURN(1); } + if (param->tablesample_clause_param && !param->has_double_value()) + { + if (param->set_tablesample_clause_param(param->val_real())) + DBUG_RETURN(true); + } } } /* diff --git a/sql/sql_string.cc b/sql/sql_string.cc index b6bdb9365bb0c..30ccf25025124 100644 --- a/sql/sql_string.cc +++ b/sql/sql_string.cc @@ -604,6 +604,18 @@ bool Binary_string::append_ulonglong(ulonglong val) return FALSE; } + +bool String::append_double(double d) +{ + if (realloc(str_length+FLOATING_POINT_BUFFER+2)) + return TRUE; + + qs_append(d); + + return FALSE; +} + + /* Append a string in the given charset to the string with character set recoding diff --git a/sql/sql_string.h b/sql/sql_string.h index ae88a28dd9854..c214172bf7bcc 100644 --- a/sql/sql_string.h +++ b/sql/sql_string.h @@ -1062,6 +1062,8 @@ class String: public Charset, public Binary_string return append(s.str, s.length, cs); } + bool append_double(double d); + // Append a wide character bool append_wc(my_wc_t wc) { diff --git a/sql/sql_tablesample.h b/sql/sql_tablesample.h new file mode 100644 index 0000000000000..0d2658fac7c32 --- /dev/null +++ b/sql/sql_tablesample.h @@ -0,0 +1,59 @@ +/* Copyright (c) 2026 Dearsh Oberoi + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#ifndef SQL_TABLESAMPLE_INCLUDED +#define SQL_TABLESAMPLE_INCLUDED + +#include "sql_alloc.h" +#include "my_global.h" +#include "item.h" + +enum tablesample_method_enum +{ + TABLESAMPLE_SYSTEM= 0, + TABLESAMPLE_BERNOULLI +}; + +class THD; + +class Lex_tablesample: public Sql_alloc +{ +private: + enum tablesample_method_enum sampling_method; + Item *sampling_percentage; + +public: + Lex_tablesample(enum tablesample_method_enum method, Item *percentage) : + sampling_method(method), sampling_percentage(percentage) {} + + int fix_tablesample_fields(THD *thd) + { + DBUG_ENTER("Lex_tablesample::fix_tablesample_fields"); + DBUG_ASSERT(thd); + bool err= sampling_percentage->fix_fields_if_needed(thd, NULL); + if(err) + DBUG_RETURN(1); + if (sampling_percentage->const_item()) + { + double d= sampling_percentage->val_real(); + if (d < 0.0 || d > 100.0) + DBUG_RETURN(1); + } + + DBUG_RETURN(0); + } +}; + +#endif \ No newline at end of file diff --git a/sql/sql_type.h b/sql/sql_type.h index 8a5e30e257551..47ffd6e2fc9a2 100644 --- a/sql/sql_type.h +++ b/sql/sql_type.h @@ -4106,7 +4106,11 @@ class Type_handler { return false; } - virtual bool is_limit_clause_valid_type() const + virtual bool is_numeric_clause_valid_type() const + { + return false; + } + virtual bool is_tablesample_clause_valid_type() const { return false; } @@ -4989,6 +4993,7 @@ class Type_handler_numeric: public Type_handler override; bool Item_char_typecast_fix_length_and_dec(Item_char_typecast *) const override; + bool is_tablesample_clause_valid_type() const override { return true; } }; @@ -5477,7 +5482,7 @@ class Type_handler_int_result: public Type_handler_numeric return attr->unsigned_flag ? DYN_COL_UINT : DYN_COL_INT; } bool is_order_clause_position_type() const override { return true; } - bool is_limit_clause_valid_type() const override { return true; } + bool is_numeric_clause_valid_type() const override { return true; } virtual ~Type_handler_int_result() = default; const Type_handler *type_handler_for_comparison() const override; int stored_field_cmp_to_item(THD *thd, Field *field, Item *item) const override; diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index c2fe9974c42df..5bc1edfd81ab3 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -198,6 +198,7 @@ void _CONCAT_UNDERSCORED(turn_parser_debug_on,yyparse)() { // Master_info_file, enum_master_use_gtid, std::optional #include "rpl_master_info_file.h" +#include "sql_tablesample.h" } %union { @@ -255,6 +256,7 @@ void _CONCAT_UNDERSCORED(turn_parser_debug_on,yyparse)() SQL_I_List *select_order; Lex_select_lock select_lock; Lex_select_limit select_limit; + Lex_tablesample *tablesample; Lex_order_limit_lock *order_limit_lock; struct { bool with_unique_keys; @@ -365,6 +367,7 @@ void _CONCAT_UNDERSCORED(turn_parser_debug_on,yyparse)() enum Column_definition::enum_column_versioning vers_column_versioning; enum plsql_cursor_attr_t plsql_cursor_attr; enum Alter_info::enum_alter_table_algorithm alter_table_algo_val; + enum tablesample_method_enum tblsmpl_method; enum_master_use_gtid master_use_gtid; privilege_t privilege; struct @@ -812,6 +815,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); %token BACKUP_SYM %token BEGIN_MARIADB_SYM /* SQL-2003-R, PLSQL-R */ %token BEGIN_ORACLE_SYM /* SQL-2003-R, PLSQL-R */ +%token BERNOULLI %token BINLOG_SYM %token BIT_SYM /* MYSQL-FUNC */ %token BLOCK_SYM @@ -1165,6 +1169,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); %token SYSTEM /* SQL-2011-R */ %token SYSTEM_TIME_SYM /* SQL-2011-R */ %token TABLES +%token TABLESAMPLE_SYM /* SQL-2016-R */ %token TABLESPACE %token TABLE_CHECKSUM_SYM %token TABLE_NAME_SYM /* SQL-2003-N */ @@ -1629,6 +1634,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); opt_versioning_interval_start json_default_literal set_expr_misc + tablesample_percentage %type sql_statement_name @@ -1638,6 +1644,8 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); %type opt_vers_auto_part +%type opt_tablesample_clause + %type param_marker %type @@ -1998,6 +2006,7 @@ rule: %type condition_information_item; %type condition_information_item_name; %type condition_information; +%type tablesample_method; %type row_field_name row_field_definition %type row_field_definition_list row_type_body @@ -12861,7 +12870,7 @@ join_table_parens: table_primary_ident: table_ident opt_use_partition opt_for_system_time_clause - opt_table_alias_clause opt_key_definition + opt_table_alias_clause opt_key_definition opt_tablesample_clause { if (!($$= Select->add_table_to_list(thd, $1, $4, 0, @@ -12872,6 +12881,8 @@ table_primary_ident: MYSQL_YYABORT; if ($3) $$->vers_conditions= Lex->vers_conditions; + if ($6) + $$->tablesample_clause= $6; } ; @@ -13080,6 +13091,47 @@ opt_having_clause: } ; +tablesample_method: + SYSTEM { $$= tablesample_method_enum::TABLESAMPLE_SYSTEM; } + | BERNOULLI { $$= tablesample_method_enum::TABLESAMPLE_BERNOULLI; } + ; + +tablesample_percentage: + ident_cli + { + if (unlikely(!($$= Lex->create_item_tablesample(thd, &$1)))) + MYSQL_YYABORT; + } + | ident_cli '.' ident_cli + { + if (unlikely(!($$= Lex->create_item_tablesample(thd, &$1, &$3)))) + MYSQL_YYABORT; + } + | param_marker + { + $1->tablesample_clause_param= TRUE; + } + | NUM_literal { $$= $1; } + ; + +opt_tablesample_clause: + /* empty */ + { $$= NULL; } + | TABLESAMPLE_SYM tablesample_method '(' tablesample_percentage ')' + { + $$ = new (thd->mem_root) Lex_tablesample($2, $4); + if (unlikely(!$$)) + YYABORT; + if ($4->basic_const_item()) { + double num = $4->val_real(); + if (num != 0.0 && num != 100.0) + Lex->set_stmt_unsafe(LEX::BINLOG_STMT_UNSAFE_TABLESAMPLE); + } else { + Lex->set_stmt_unsafe(LEX::BINLOG_STMT_UNSAFE_TABLESAMPLE); + } + } + ; + /* group by statement in select */ diff --git a/sql/table.h b/sql/table.h index d04521e889b07..57e39c6bc5fa3 100644 --- a/sql/table.h +++ b/sql/table.h @@ -86,6 +86,7 @@ class MYSQL_LOG; struct rpl_group_info; class Opt_hints_qb; class Opt_hints_table; +class Lex_tablesample; /* Used to identify NESTED_JOIN structures within a join (applicable only to @@ -2884,6 +2885,8 @@ struct TABLE_LIST qc_engine_callback callback_func; thr_lock_type lock_type; + Lex_tablesample *tablesample_clause; + /* Two fields below are set during parsing this table reference in the cases when the table reference can be potentially a reference to a CTE table. From 8f72b6a349a6ae537386787eba16c1af2eea7656 Mon Sep 17 00:00:00 2001 From: deo002 Date: Sun, 5 Jul 2026 21:43:13 +0530 Subject: [PATCH 2/7] feat(index): disable indexes in presence of tablesample clause Signed-off-by: deo002 --- sql/sql_base.cc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 6a66f94db1892..0b65effcb749d 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -8661,6 +8661,14 @@ bool setup_tables(THD *thd, Name_resolution_context *context, my_error(ER_SYNTAX_ERROR, MYF(0)); DBUG_RETURN(1); } + + // disable usage of indexes in presence of tablesample clause + // because it will cause the optimizer to choose an unwanted access + // path, wrong join strategy etc + table_list->table->keys_in_use_for_query.clear_all(); + table_list->table->keys_in_use_for_group_by.clear_all(); + table_list->table->keys_in_use_for_order_by.clear_all(); + table_list->table->keys_in_use_for_rowid_filter.clear_all(); } } From fe2dbd7301b68ffd9d591c31a2f2911c000ab719 Mon Sep 17 00:00:00 2001 From: deo002 Date: Thu, 9 Jul 2026 22:07:19 +0530 Subject: [PATCH 3/7] MDEV-38992 Cost modelling and index supression for TABLESAMPLE 1. Disable indexes by clearing index bitmaps. 2. Add cost estimates such as records retrieved, index and row retrieval costs for both sampling methods so that optimzer comes up with an apt best plan for execution. Signed-off-by: deo002 --- sql/opt_hints.cc | 18 ++++++++++++++++++ sql/sql_base.cc | 40 ++++++++++++++++++++++------------------ sql/sql_select.cc | 41 ++++++++++++++++++++++++++++++++++++++--- sql/sql_statistics.cc | 5 +++++ sql/sql_tablesample.h | 32 +++++++++++++++++++++++--------- sql/table.cc | 18 ++++++++++++++++++ 6 files changed, 124 insertions(+), 30 deletions(-) diff --git a/sql/opt_hints.cc b/sql/opt_hints.cc index 7d92f3c0adbe6..a1fe754265c47 100644 --- a/sql/opt_hints.cc +++ b/sql/opt_hints.cc @@ -958,6 +958,24 @@ void Opt_hints_table::update_index_hint_map(Key_map *keys_to_use, bool Opt_hints_table::update_index_hint_maps(THD *thd, TABLE *tbl) { + /* + A TABLESAMPLE clause forces a sampling scan of the table and + index-based access can bias the result. Ignore any index hints + (old- or new-style) entirely and make sure no key is considered + usable, regardless of what the hints say. + */ + if (tbl->pos_in_table_list && tbl->pos_in_table_list->tablesample_clause) + { + tbl->keys_in_use_for_query.clear_all(); + tbl->keys_in_use_for_group_by.clear_all(); + tbl->keys_in_use_for_order_by.clear_all(); + tbl->keys_in_use_for_rowid_filter.clear_all(); + tbl->covering_keys.clear_all(); + tbl->force_index= tbl->force_index_join= tbl->force_index_group= + tbl->force_index_order= false; + return true; // handled: caller must not also run process_index_hints() + } + if (!is_fixed(INDEX_HINT_ENUM) && !is_fixed(JOIN_INDEX_HINT_ENUM) && !is_fixed(GROUP_INDEX_HINT_ENUM) && !is_fixed(ORDER_INDEX_HINT_ENUM) && !is_fixed(ROWID_FILTER_HINT_ENUM)) diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 0b65effcb749d..170c1d4c9c0c2 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -8558,6 +8558,14 @@ bool setup_tables(THD *thd, Name_resolution_context *context, table_list->alias); } + if (table_list->tablesample_clause && (get_table_category( + table_list->db, table_list->table_name) != TABLE_CATEGORY_USER || + table_list->tablesample_clause->fix_tablesample_fields(thd))) + { + my_error(ER_SYNTAX_ERROR, MYF(0)); + DBUG_RETURN(1); + } + if (!table_list->opt_hints_table || !table_list->opt_hints_table->update_index_hint_maps(thd, table)) { @@ -8610,6 +8618,14 @@ bool setup_tables(THD *thd, Name_resolution_context *context, table->maybe_null= table_list->maybe_null_exec; table->pos_in_table_list= table_list; + if (table_list->tablesample_clause && (get_table_category( + table_list->db, table_list->table_name) != TABLE_CATEGORY_USER || + table_list->tablesample_clause->fix_tablesample_fields(thd))) + { + my_error(ER_SYNTAX_ERROR, MYF(0)); + DBUG_RETURN(1); + } + if (!table_list->opt_hints_table || !table_list->opt_hints_table->update_index_hint_maps(thd, table)) { @@ -8629,6 +8645,12 @@ bool setup_tables(THD *thd, Name_resolution_context *context, table_list; table_list= table_list->next_local) { + if (table_list->tablesample_clause && table_list->is_view_or_derived()) + { + my_error(ER_SYNTAX_ERROR, MYF(0)); + DBUG_RETURN(1); + } + if (table_list->is_merged_derived() && table_list->merge_underlying_list) { Query_arena *arena, backup; @@ -8652,24 +8674,6 @@ bool setup_tables(THD *thd, Name_resolution_context *context, } DBUG_ASSERT(item == table_list->jtbm_subselect->optimizer); } - - if (table_list->tablesample_clause) { - if (table_list->is_view_or_derived() || - get_table_category(table_list->db, table_list->table_name) != TABLE_CATEGORY_USER || - table_list->tablesample_clause->fix_tablesample_fields(thd)) - { - my_error(ER_SYNTAX_ERROR, MYF(0)); - DBUG_RETURN(1); - } - - // disable usage of indexes in presence of tablesample clause - // because it will cause the optimizer to choose an unwanted access - // path, wrong join strategy etc - table_list->table->keys_in_use_for_query.clear_all(); - table_list->table->keys_in_use_for_group_by.clear_all(); - table_list->table->keys_in_use_for_order_by.clear_all(); - table_list->table->keys_in_use_for_rowid_filter.clear_all(); - } } /* Precompute and store the row types of NATURAL/USING joins. */ diff --git a/sql/sql_select.cc b/sql/sql_select.cc index dc9e7b6113333..d114aeb33bd18 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -71,6 +71,7 @@ #include "derived_handler.h" #include "opt_hints.h" #include "opt_group_by_cardinality.h" +#include "sql_tablesample.h" /* A key part number that means we're using a fulltext scan. @@ -16886,6 +16887,7 @@ void JOIN_TAB::estimate_scan_time() handler *file= table->file; double row_copy_cost, copy_cost; ALL_READ_COST * const cost= &cached_scan_and_compare_cost; + Lex_tablesample *sampling_info; cost->reset(); cached_covering_key= MAX_KEY; @@ -16918,9 +16920,42 @@ void JOIN_TAB::estimate_scan_time() } else { - cost->row_cost= file->ha_scan_time(records); - read_time= file->cost(cost->row_cost); - row_copy_cost= 0; // Included in ha_scan_time + sampling_info= table->pos_in_table_list->tablesample_clause; + if (unlikely(sampling_info && sampling_info->get_sampling_method() == + tablesample_method_enum::TABLESAMPLE_SYSTEM)) + { + cached_covering_key= table->s->primary_key; + if (cached_covering_key != MAX_KEY) + { + if (file->is_clustering_key(cached_covering_key)) + { + cost->index_cost= + file->ha_keyread_clustered_time(cached_covering_key, records, records, 0); + read_time= file->cost(cost->index_cost); + row_copy_cost= file->ROW_COPY_COST; + } + else + { + cost->index_cost= + file->ha_keyread_time(cached_covering_key, records, records, 0); + cost->row_cost= file->ha_rnd_pos_time(records); + read_time= file->cost(cost->row_cost) + file->cost(cost->index_cost); + row_copy_cost= 0; // included in ha_rnd_pos_time + } + } + else + { + cost->row_cost= file->ha_scan_time(records); + read_time= file->cost(cost->row_cost); + row_copy_cost= 0; // Included in ha_scan_time + } + } + else + { + cost->row_cost= file->ha_scan_time(records); + read_time= file->cost(cost->row_cost); + row_copy_cost= 0; // Included in ha_scan_time + } } } } diff --git a/sql/sql_statistics.cc b/sql/sql_statistics.cc index 06ae3531c426f..43a983cb458b1 100644 --- a/sql/sql_statistics.cc +++ b/sql/sql_statistics.cc @@ -34,6 +34,7 @@ #include "sql_show.h" #include "sql_partition.h" #include "sql_alter.h" // RENAME_STAT_PARAMS +#include "sql_tablesample.h" #include #include @@ -4161,6 +4162,10 @@ void set_statistics_for_table(THD *thd, TABLE *table) table->used_stat_records= table->file->stats.records; #endif + if (table->pos_in_table_list && table->pos_in_table_list->tablesample_clause) + table->used_stat_records= (ha_rows)(table->used_stat_records * + table->pos_in_table_list->tablesample_clause->get_sampling_percentage_fraction()); + KEY *key_info, *key_info_end; for (key_info= table->key_info, key_info_end= key_info+table->s->keys; key_info < key_info_end; key_info++) diff --git a/sql/sql_tablesample.h b/sql/sql_tablesample.h index 0d2658fac7c32..16f293f84e24f 100644 --- a/sql/sql_tablesample.h +++ b/sql/sql_tablesample.h @@ -22,7 +22,8 @@ enum tablesample_method_enum { - TABLESAMPLE_SYSTEM= 0, + TABLESAMPLE_NONE= 0, + TABLESAMPLE_SYSTEM, TABLESAMPLE_BERNOULLI }; @@ -31,8 +32,10 @@ class THD; class Lex_tablesample: public Sql_alloc { private: - enum tablesample_method_enum sampling_method; + enum tablesample_method_enum sampling_method= + tablesample_method_enum::TABLESAMPLE_NONE; Item *sampling_percentage; + double percentage= 0.0; public: Lex_tablesample(enum tablesample_method_enum method, Item *percentage) : @@ -46,14 +49,25 @@ class Lex_tablesample: public Sql_alloc if(err) DBUG_RETURN(1); if (sampling_percentage->const_item()) - { - double d= sampling_percentage->val_real(); - if (d < 0.0 || d > 100.0) - DBUG_RETURN(1); - } - - DBUG_RETURN(0); + { + double d= sampling_percentage->val_real(); + if (d < 0.0 || d > 100.0) + DBUG_RETURN(1); + percentage= d / 100.0; } + + DBUG_RETURN(0); + } + + double get_sampling_percentage_fraction() const + { + return percentage; + } + + tablesample_method_enum get_sampling_method() const + { + return sampling_method; + } }; #endif \ No newline at end of file diff --git a/sql/table.cc b/sql/table.cc index acc6377de1b50..2b3c1515876e2 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -9084,6 +9084,24 @@ Item_subselect *TABLE_LIST::containing_subselect() */ bool TABLE_LIST::process_index_hints(TABLE *tbl) { + /* + A TABLESAMPLE clause forces a sampling scan of the table and + index-based access can bias the result. Ignore any index hints + (old- or new-style) entirely and make sure no key is considered + usable, regardless of what the hints say. + */ + if (tbl->pos_in_table_list && tbl->pos_in_table_list->tablesample_clause) + { + tbl->keys_in_use_for_query.clear_all(); + tbl->keys_in_use_for_group_by.clear_all(); + tbl->keys_in_use_for_order_by.clear_all(); + tbl->keys_in_use_for_rowid_filter.clear_all(); + tbl->covering_keys.clear_all(); + tbl->force_index= tbl->force_index_join= tbl->force_index_group= + tbl->force_index_order= false; + return false; + } + /* initialize the result variables */ tbl->keys_in_use_for_query= tbl->keys_in_use_for_group_by= tbl->keys_in_use_for_order_by= tbl->keys_in_use_for_rowid_filter= From 3e31fa0c99d8733ac29d2c79db9790a384206a84 Mon Sep 17 00:00:00 2001 From: deo002 Date: Sun, 9 Aug 2026 14:49:49 +0530 Subject: [PATCH 4/7] feat(tablesample): Add bernoulli sampling code --- mysql-test/main/tablesample.test | 7 ++++ sql/CMakeLists.txt | 1 + sql/records.cc | 61 ++++++++++++++++++++++++++++++++ sql/records.h | 3 ++ sql/sql_select.cc | 22 ++++++++---- sql/sql_select.h | 4 ++- sql/sql_tablesample.cc | 38 ++++++++++++++++++++ sql/sql_tablesample.h | 28 ++++++--------- 8 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 sql/sql_tablesample.cc diff --git a/mysql-test/main/tablesample.test b/mysql-test/main/tablesample.test index 460e681053530..1610c851974dc 100644 --- a/mysql-test/main/tablesample.test +++ b/mysql-test/main/tablesample.test @@ -119,6 +119,13 @@ SELECT * FROM (SELECT * FROM t1) AS derived_tbl TABLESAMPLE SYSTEM (50); WITH cte_tbl AS (SELECT * FROM t1) SELECT * FROM cte_tbl TABLESAMPLE BERNOULLI (10); +# INSERT INTO t1 VALUES (1, 1), (1, 2), (1, 3), (1, 4), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10); +# INSERT INTO t2 VALUES (1, 1), (1, 2), (1, 3), (1, 4), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10); + +# SELECT * FROM t1 TABLESAMPLE BERNOULLI (50); + +# SELECT * FROM t1 TABLESAMPLE BERNOULLI (50) JOIN t2 TABLESAMPLE BERNOULLI (50) ON t1.a = t2.a; + # # Cleanup # diff --git a/sql/CMakeLists.txt b/sql/CMakeLists.txt index 429a85f9cac30..3ae7b20f63ac4 100644 --- a/sql/CMakeLists.txt +++ b/sql/CMakeLists.txt @@ -185,6 +185,7 @@ SET (SQL_SOURCE sql_type_string.cc sql_type_geom.cc sql_type_vector.cc item_windowfunc.cc sql_window.cc + sql_tablesample.cc sql_cte.cc item_vers.cc sql_sequence.cc sql_sequence.h ha_sequence.h diff --git a/sql/records.cc b/sql/records.cc index 5a7f5385f06c5..0e1d9021c2a16 100644 --- a/sql/records.cc +++ b/sql/records.cc @@ -30,9 +30,12 @@ #include "sql_class.h" // THD #include "sql_base.h" #include "sql_sort.h" // SORT_ADDON_FIELD +#include "sql_tablesample.h" static int rr_quick(READ_RECORD *info); int rr_sequential(READ_RECORD *info); +int rr_sampling_bernoulli(READ_RECORD *info); +int rr_sampling_system(READ_RECORD *info); static int rr_from_tempfile(READ_RECORD *info); template static int rr_unpack_from_tempfile(READ_RECORD *info); template static int rr_unpack_from_buffer(READ_RECORD *info); @@ -187,6 +190,8 @@ bool init_read_record(READ_RECORD *info,THD *thd, TABLE *table, const bool using_addon_fields= filesort && filesort->using_addon_fields(); bool using_packed_sortkeys= filesort && filesort->using_packed_sortkeys(); + const bool has_tablesample= table->pos_in_table_list + && table->pos_in_table_list->tablesample_clause; bzero((char*) info,sizeof(*info)); info->thd=thd; @@ -316,6 +321,21 @@ bool init_read_record(READ_RECORD *info,THD *thd, TABLE *table, DBUG_RETURN(1); } } + else if (has_tablesample) + { + DBUG_PRINT("info",("using rr_sampling")); + Lex_tablesample *tablesample_clause= + table->pos_in_table_list->tablesample_clause; + enum tablesample_method_enum sampling_method= + tablesample_clause->get_sampling_method(); + if (sampling_method == tablesample_method_enum::TABLESAMPLE_BERNOULLI) + info->read_record_func= rr_sampling_bernoulli; + else + info->read_record_func= rr_sampling_system; + if (unlikely(table->file->ha_rnd_init_with_error(1))) + DBUG_RETURN(1); + tablesample_clause->seed_sample_rand(&info->sample_rand); + } else { DBUG_PRINT("info",("using rr_sequential")); @@ -515,6 +535,47 @@ int rr_sequential(READ_RECORD *info) } +int rr_sampling_bernoulli(READ_RECORD *info) +{ + int tmp; + const double p= info->table->pos_in_table_list->tablesample_clause-> + get_sampling_percentage_fraction(); + + for (;;) + { + tmp= info->table->file->ha_rnd_next(info->record()); + if (tmp) + { + tmp= rr_handle_error(info, tmp); + break; + } + if (my_rnd(&info->sample_rand) < p) + break; + } + return tmp; +} + + +int rr_sampling_system(READ_RECORD *info) +{ + int tmp; + const double p= info->table->pos_in_table_list->tablesample_clause-> + get_sampling_percentage_fraction(); + + for (;;) + { + tmp= info->table->file->ha_rnd_next(info->record()); + if (tmp) + { + tmp= rr_handle_error(info, tmp); + break; + } + if (my_rnd(&info->sample_rand) < p) + break; + } + return tmp; +} + static int rr_from_tempfile(READ_RECORD *info) { int tmp; diff --git a/sql/records.h b/sql/records.h index 48ce8e2c91752..dc7e167002297 100644 --- a/sql/records.h +++ b/sql/records.h @@ -67,6 +67,9 @@ struct READ_RECORD uchar *rec_buf; /* to read field values after filesort */ uchar *cache,*cache_pos,*cache_end,*read_positions; + // initialised only if dealing with tablesample clause + struct my_rnd_struct sample_rand; + /* Structure storing information about sorting */ diff --git a/sql/sql_select.cc b/sql/sql_select.cc index d114aeb33bd18..cde3fda2bb3cb 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -3459,6 +3459,7 @@ int JOIN::optimize_stage2() tab->type != JT_NEXT && tab->type != JT_FT && tab->type != JT_REF_OR_NULL && + tab->type != JT_SAMPLE && ((order && simple_order) || (group_list && simple_group))) { if (add_ref_to_table_cond(thd,tab)) { @@ -8815,6 +8816,8 @@ best_access_path(JOIN *join, table_map spl_pd_boundary= 0; Loose_scan_opt loose_scan_opt; struct best_plan best; + bool is_tablesample= table->pos_in_table_list && + table->pos_in_table_list->tablesample_clause; Json_writer_object trace_wrapper(thd, "best_access_path"); DBUG_ENTER("best_access_path"); @@ -9948,14 +9951,14 @@ best_access_path(JOIN *join, { /* No usable key, use table scan */ cost= s->cached_scan_and_compare_cost; - type= JT_ALL; + type= is_tablesample ? JT_SAMPLE : JT_ALL; } } } else // table scan { cost= s->cached_scan_and_compare_cost; - type= JT_ALL; + type= is_tablesample ? JT_SAMPLE : JT_ALL; } /* Cache result for other calls */ s->cached_forced_index_type= type; @@ -10035,7 +10038,8 @@ best_access_path(JOIN *join, { trace_access_scan. add("access_type", - type == JT_ALL ? scan_type : join_type_str[type]); + (type == JT_ALL || type == JT_SAMPLE) ? + scan_type : join_type_str[type]); if (type == JT_RANGE) trace_access_scan. add("range_index", table->key_info[s->quick->index].name); @@ -13465,6 +13469,8 @@ bool JOIN::get_best_combination() j->bush_root_tab= sjm_nest_root; form= table[tablenr]= j->table; + bool is_tablesample= form->pos_in_table_list && + form->pos_in_table_list->tablesample_clause; form->reginfo.join_tab=j; DBUG_PRINT("info",("type: %d", j->type)); if (j->type == JT_CONST) @@ -13486,7 +13492,7 @@ bool JOIN::get_best_combination() j->index= cur_pos->forced_index; } else - j->type= JT_ALL; + j->type= is_tablesample ? JT_SAMPLE : JT_ALL; if (cur_pos->use_join_buffer && tablenr != const_tables) full_join= 1; @@ -16079,6 +16085,7 @@ uint check_join_cache_usage(JOIN_TAB *tab, case JT_NEXT: case JT_ALL: case JT_RANGE: + case JT_SAMPLE: if (hint_disables_bnl) goto no_join_cache; if (cache_level == 1) @@ -16170,7 +16177,8 @@ uint check_join_cache_usage(JOIN_TAB *tab, } no_join_cache: - if (tab->type != JT_ALL && tab->type != JT_RANGE && tab->is_ref_for_hash_join()) + if (tab->type != JT_ALL && tab->type != JT_RANGE && + tab->type != JT_SAMPLE && tab->is_ref_for_hash_join()) { tab->type= JT_ALL; tab->ref.key_parts= 0; @@ -16253,6 +16261,7 @@ void check_join_cache_usage_for_tables(JOIN *join, ulonglong options, case JT_NEXT: case JT_ALL: case JT_RANGE: + case JT_SAMPLE: tab->used_join_cache_level= check_join_cache_usage(tab, options, no_jbuf_after, idx, @@ -16539,6 +16548,7 @@ make_join_readinfo(JOIN *join, ulonglong options, uint no_jbuf_after) case JT_ALL: case JT_RANGE: case JT_HASH: + case JT_SAMPLE: { bool have_quick_select= tab->select && tab->select->quick; /* @@ -17052,7 +17062,7 @@ double JOIN_TAB::get_examined_rows() DBUG_ASSERT(examined_rows == sel->quick->records); } else if (type == JT_NEXT || type == JT_ALL || type == JT_RANGE || - type == JT_HASH || type == JT_HASH_NEXT) + type == JT_HASH || type == JT_HASH_NEXT || type == JT_SAMPLE) { if (limit) { diff --git a/sql/sql_select.h b/sql/sql_select.h index 23a927bfcb14e..48c07480152fd 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -434,7 +434,9 @@ enum join_type Shown as "hash_index_merge" in EXPLAIN. */ - JT_HASH_INDEX_MERGE + JT_HASH_INDEX_MERGE, + JT_SAMPLE, + JT_HASH_SAMPLE }; class JOIN; diff --git a/sql/sql_tablesample.cc b/sql/sql_tablesample.cc new file mode 100644 index 0000000000000..b4e3c760ac6a1 --- /dev/null +++ b/sql/sql_tablesample.cc @@ -0,0 +1,38 @@ +/* Copyright (c) 2026 Dearsh Oberoi + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#include "sql_tablesample.h" +#include "sql_class.h" + +int Lex_tablesample::fix_tablesample_fields(THD *thd) +{ +DBUG_ENTER("Lex_tablesample::fix_tablesample_fields"); +DBUG_ASSERT(thd); +bool err= sampling_percentage->fix_fields_if_needed(thd, NULL); +if(err) + DBUG_RETURN(1); +if (sampling_percentage->const_item()) +{ + double d= sampling_percentage->val_real(); + if (d < 0.0 || d > 100.0) + DBUG_RETURN(1); + percentage= d / 100.0; + + seed1= (ulong)(my_rnd(&thd->rand) * (double) 0xFFFFFFFFUL); + seed2= (ulong)(my_rnd(&thd->rand) * (double) 0xFFFFFFFFUL); +} + +DBUG_RETURN(0); +} \ No newline at end of file diff --git a/sql/sql_tablesample.h b/sql/sql_tablesample.h index 16f293f84e24f..5ca1c76f6c866 100644 --- a/sql/sql_tablesample.h +++ b/sql/sql_tablesample.h @@ -16,9 +16,10 @@ #ifndef SQL_TABLESAMPLE_INCLUDED #define SQL_TABLESAMPLE_INCLUDED -#include "sql_alloc.h" + #include "my_global.h" #include "item.h" +#include "sql_alloc.h" enum tablesample_method_enum { @@ -36,28 +37,14 @@ class Lex_tablesample: public Sql_alloc tablesample_method_enum::TABLESAMPLE_NONE; Item *sampling_percentage; double percentage= 0.0; + ulong seed1= 0; + ulong seed2= 0; public: Lex_tablesample(enum tablesample_method_enum method, Item *percentage) : sampling_method(method), sampling_percentage(percentage) {} - int fix_tablesample_fields(THD *thd) - { - DBUG_ENTER("Lex_tablesample::fix_tablesample_fields"); - DBUG_ASSERT(thd); - bool err= sampling_percentage->fix_fields_if_needed(thd, NULL); - if(err) - DBUG_RETURN(1); - if (sampling_percentage->const_item()) - { - double d= sampling_percentage->val_real(); - if (d < 0.0 || d > 100.0) - DBUG_RETURN(1); - percentage= d / 100.0; - } - - DBUG_RETURN(0); - } + int fix_tablesample_fields(THD *thd); double get_sampling_percentage_fraction() const { @@ -68,6 +55,11 @@ class Lex_tablesample: public Sql_alloc { return sampling_method; } + + void seed_sample_rand(my_rnd_struct *out) const + { + my_rnd_init(out, seed1, seed2); + } }; #endif \ No newline at end of file From 614adba450d5889906b2ea6c8e4f3f0531df6445 Mon Sep 17 00:00:00 2001 From: deo002 Date: Sat, 22 Aug 2026 16:06:28 +0530 Subject: [PATCH 5/7] feat(system): Add SYSTEM sampling method Signed-off-by: deo002 --- include/myisam.h | 2 + mysql-test/main/tablesample.result | 68 ++++++++++++++++-- mysql-test/main/tablesample.test | 71 +++++++++++++++++-- sql/CMakeLists.txt | 1 + sql/handler.cc | 39 ++++++++++ sql/handler.h | 11 +++ sql/records.cc | 64 ++++++++++++++--- sql/records.h | 12 +++- sql/share/errmsg-utf8.txt | 4 ++ sql/sql_base.cc | 4 +- sql/sql_explain.cc | 10 +-- sql/sql_select.cc | 46 ++++++------ sql/sql_select.h | 12 +++- sql/sql_tablesample.cc | 57 ++++++++++----- sql/sql_tablesample.h | 36 +++++++++- storage/myisam/CMakeLists.txt | 5 +- storage/myisam/ha_myisam.cc | 6 ++ storage/myisam/ha_myisam.h | 1 + storage/myisam/mi_random_dive.c | 110 +++++++++++++++++++++++++++++ 19 files changed, 491 insertions(+), 68 deletions(-) create mode 100644 storage/myisam/mi_random_dive.c diff --git a/include/myisam.h b/include/myisam.h index 5c75f3fbe28cf..3179994433ac8 100644 --- a/include/myisam.h +++ b/include/myisam.h @@ -268,6 +268,8 @@ extern int mi_rlast(struct st_myisam_info *file,uchar *buf,int inx); extern int mi_rnext(struct st_myisam_info *file,uchar *buf,int inx); extern int mi_rnext_same(struct st_myisam_info *info, uchar *buf); extern int mi_rprev(struct st_myisam_info *file,uchar *buf,int inx); +extern int mi_random_dive(struct st_myisam_info *file, int inx, uchar *buf, + struct my_rnd_struct *rand_state); extern int mi_rrnd(struct st_myisam_info *file,uchar *buf, my_off_t pos); extern int mi_scan_init(struct st_myisam_info *file); extern int mi_scan(struct st_myisam_info *file,uchar *buf); diff --git a/mysql-test/main/tablesample.result b/mysql-test/main/tablesample.result index 8473b23f78264..7ccf06fc75843 100644 --- a/mysql-test/main/tablesample.result +++ b/mysql-test/main/tablesample.result @@ -1,6 +1,6 @@ DROP TABLE IF EXISTS t1, t2; DROP VIEW IF EXISTS v1; -CREATE TABLE t1 (a INT, b INT, KEY idx_b(b)); +CREATE TABLE t1 (a INT, b INT, PRIMARY KEY (b)); CREATE TABLE t2 (a INT, c INT); CREATE VIEW v1 AS SELECT * FROM t1; SELECT * FROM t1 TABLESAMPLE SYSTEM (10); @@ -34,7 +34,7 @@ Warnings: Note 1305 PROCEDURE test.p1 does not exist CREATE PROCEDURE p1(IN sample_pct INT) BEGIN -SELECT * FROM t1 TABLESAMPLE SYSTEM (sample_pct); +SELECT * FROM t1 TABLESAMPLE BERNOULLI (sample_pct); END// CALL p1(40); a b @@ -45,7 +45,7 @@ CREATE PROCEDURE p1(IN sample_pct DECIMAL) BEGIN SELECT * FROM t1 TABLESAMPLE SYSTEM (sample_pct); END// -CALL p1(40.11); +CALL p1(20.11); a b Warnings: Note 1265 Data truncated for column 'sample_pct' at row 0 @@ -70,10 +70,68 @@ SELECT * FROM information_schema.tables TABLESAMPLE BERNOULLI (5); ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use SELECT * FROM mysql.user TABLESAMPLE SYSTEM (10); ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use -SELECT * FROM (SELECT * FROM t1) AS derived_tbl TABLESAMPLE SYSTEM (50); -ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'TABLESAMPLE SYSTEM (50)' at line 1 +SELECT * FROM (SELECT * FROM t1) AS derived_tbl TABLESAMPLE SYSTEM (30); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'TABLESAMPLE SYSTEM (30)' at line 1 WITH cte_tbl AS (SELECT * FROM t1) SELECT * FROM cte_tbl TABLESAMPLE BERNOULLI (10); ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +CREATE TABLE t_no_pk (a INT, b INT); +CREATE TABLE t_pk (a INT PRIMARY KEY, b INT); +SELECT * FROM t_no_pk TABLESAMPLE SYSTEM (10); +a b +Warnings: +Note 4272 TABLESAMPLE SYSTEM requires a primary key on table 't_no_pk'; using BERNOULLI sampling instead +SHOW WARNINGS; +Level Code Message +Note 4272 TABLESAMPLE SYSTEM requires a primary key on table 't_no_pk'; using BERNOULLI sampling instead +SELECT * FROM t_pk TABLESAMPLE SYSTEM (10); +a b +SHOW WARNINGS; +Level Code Message +SELECT * FROM t_pk TABLESAMPLE SYSTEM (30); +a b +SHOW WARNINGS; +Level Code Message +SELECT * FROM t_pk TABLESAMPLE SYSTEM (30.01); +a b +Warnings: +Note 4273 TABLESAMPLE SYSTEM on table 't_pk' requested more than 30% of rows; using BERNOULLI sampling instead +SHOW WARNINGS; +Level Code Message +Note 4273 TABLESAMPLE SYSTEM on table 't_pk' requested more than 30% of rows; using BERNOULLI sampling instead +SELECT * FROM t_pk TABLESAMPLE SYSTEM (50); +a b +Warnings: +Note 4273 TABLESAMPLE SYSTEM on table 't_pk' requested more than 30% of rows; using BERNOULLI sampling instead +SHOW WARNINGS; +Level Code Message +Note 4273 TABLESAMPLE SYSTEM on table 't_pk' requested more than 30% of rows; using BERNOULLI sampling instead +PREPARE stmt3 FROM 'SELECT * FROM t_no_pk TABLESAMPLE SYSTEM (10)'; +Warnings: +Note 4272 TABLESAMPLE SYSTEM requires a primary key on table 't_no_pk'; using BERNOULLI sampling instead +EXECUTE stmt3; +a b +Warnings: +Note 4272 TABLESAMPLE SYSTEM requires a primary key on table 't_no_pk'; using BERNOULLI sampling instead +SHOW WARNINGS; +Level Code Message +Note 4272 TABLESAMPLE SYSTEM requires a primary key on table 't_no_pk'; using BERNOULLI sampling instead +ALTER TABLE t_no_pk ADD PRIMARY KEY (a); +EXECUTE stmt3; +a b +SHOW WARNINGS; +Level Code Message +DEALLOCATE PREPARE stmt3; +DROP TABLE t_no_pk, t_pk; +CREATE TABLE t_data (a INT PRIMARY KEY, b INT) ENGINE=MyISAM; +INSERT INTO t_data SELECT seq, seq * 2 FROM seq_1_to_20; +SET @save_join_cache_level= @@join_cache_level; +SET join_cache_level= 3; +EXPLAIN SELECT * FROM t_data AS t1 TABLESAMPLE BERNOULLI (20) +JOIN t_data AS t2 TABLESAMPLE BERNOULLI (30) ON t1.a = t2.a; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 sample NULL NULL NULL NULL 4 +1 SIMPLE t2 hash_sample NULL #hash#$hj: 4: test.t1.a 6 Using where; Using join buffer (flat, BNLH join) +SET join_cache_level= @save_join_cache_level; DROP TABLE t1, t2; DROP VIEW v1; diff --git a/mysql-test/main/tablesample.test b/mysql-test/main/tablesample.test index 1610c851974dc..b76359cd4124a 100644 --- a/mysql-test/main/tablesample.test +++ b/mysql-test/main/tablesample.test @@ -9,7 +9,7 @@ DROP TABLE IF EXISTS t1, t2; DROP VIEW IF EXISTS v1; --enable_warnings -CREATE TABLE t1 (a INT, b INT, KEY idx_b(b)); +CREATE TABLE t1 (a INT, b INT, PRIMARY KEY (b)); CREATE TABLE t2 (a INT, c INT); CREATE VIEW v1 AS SELECT * FROM t1; @@ -58,7 +58,7 @@ DROP PROCEDURE IF EXISTS p1; DELIMITER //; CREATE PROCEDURE p1(IN sample_pct INT) BEGIN - SELECT * FROM t1 TABLESAMPLE SYSTEM (sample_pct); + SELECT * FROM t1 TABLESAMPLE BERNOULLI (sample_pct); END// DELIMITER ;// @@ -76,7 +76,7 @@ BEGIN END// DELIMITER ;// -CALL p1(40.11); +CALL p1(20.11); DROP PROCEDURE p1; @@ -113,7 +113,7 @@ SELECT * FROM mysql.user TABLESAMPLE SYSTEM (10); # TABLESAMPLE should not work on derived tables # --error ER_PARSE_ERROR -SELECT * FROM (SELECT * FROM t1) AS derived_tbl TABLESAMPLE SYSTEM (50); +SELECT * FROM (SELECT * FROM t1) AS derived_tbl TABLESAMPLE SYSTEM (30); --error ER_SYNTAX_ERROR WITH cte_tbl AS (SELECT * FROM t1) @@ -126,6 +126,69 @@ SELECT * FROM cte_tbl TABLESAMPLE BERNOULLI (10); # SELECT * FROM t1 TABLESAMPLE BERNOULLI (50) JOIN t2 TABLESAMPLE BERNOULLI (50) ON t1.a = t2.a; +# +# TABLESAMPLE SYSTEM without a primary key falls back to BERNOULLI +# +CREATE TABLE t_no_pk (a INT, b INT); +CREATE TABLE t_pk (a INT PRIMARY KEY, b INT); + +SELECT * FROM t_no_pk TABLESAMPLE SYSTEM (10); +SHOW WARNINGS; + +SELECT * FROM t_pk TABLESAMPLE SYSTEM (10); +SHOW WARNINGS; + +# +# TABLESAMPLE SYSTEM with a sampling percentage above the +# SYSTEM_TO_BERNOULLI_MAX_FRACTION threshold falls back to +# BERNOULLI +# +SELECT * FROM t_pk TABLESAMPLE SYSTEM (30); +SHOW WARNINGS; + +SELECT * FROM t_pk TABLESAMPLE SYSTEM (30.01); +SHOW WARNINGS; + +SELECT * FROM t_pk TABLESAMPLE SYSTEM (50); +SHOW WARNINGS; + +# Same parsed statement re-executed after the table's primary key changes: +# the fallback (and its warning) must reflect the CURRENT execution's table, +# not stick around from an earlier execution. +PREPARE stmt3 FROM 'SELECT * FROM t_no_pk TABLESAMPLE SYSTEM (10)'; + +EXECUTE stmt3; +SHOW WARNINGS; + +ALTER TABLE t_no_pk ADD PRIMARY KEY (a); + +EXECUTE stmt3; +SHOW WARNINGS; + +DEALLOCATE PREPARE stmt3; +DROP TABLE t_no_pk, t_pk; + +--source include/have_sequence.inc + +CREATE TABLE t_data (a INT PRIMARY KEY, b INT) ENGINE=MyISAM; +INSERT INTO t_data SELECT seq, seq * 2 FROM seq_1_to_20; + +# SELECT * FROM t_data TABLESAMPLE SYSTEM (20); + +# SELECT * FROM t_data as t1 TABLESAMPLE BERNOULLI (20) JOIN t_data as t2 TABLESAMPLE BERNOULLI (30) ON t1.a = t2.a; + +# +# A TABLESAMPLE'd table joined via hash join (BNLH) shows "hash_sample" +# in EXPLAIN, not the misleading "hash_ALL" +# +SET @save_join_cache_level= @@join_cache_level; +SET join_cache_level= 3; + +EXPLAIN SELECT * FROM t_data AS t1 TABLESAMPLE BERNOULLI (20) + JOIN t_data AS t2 TABLESAMPLE BERNOULLI (30) ON t1.a = t2.a; + +SET join_cache_level= @save_join_cache_level; + # # Cleanup # diff --git a/sql/CMakeLists.txt b/sql/CMakeLists.txt index 3ae7b20f63ac4..ce9f914464bbd 100644 --- a/sql/CMakeLists.txt +++ b/sql/CMakeLists.txt @@ -429,6 +429,7 @@ IF(NOT CMAKE_CROSSCOMPILING OR DEFINED CMAKE_CROSSCOMPILING_EMULATOR) ADD_EXECUTABLE(gen_lex_token gen_lex_token.cc ${CMAKE_CURRENT_BINARY_DIR}/yy_mariadb.hh) TARGET_LINK_LIBRARIES(gen_lex_token mysys dbug) # required by `yy_mariadb.hh` + ADD_DEPENDENCIES(gen_lex_token GenError) # sql_yacc.yy pulls in mysqld_error.h via item.h ADD_EXECUTABLE(gen_lex_hash gen_lex_hash.cc) ENDIF() diff --git a/sql/handler.cc b/sql/handler.cc index c915ccabc28f9..b86c56d1d6c4c 100644 --- a/sql/handler.cc +++ b/sql/handler.cc @@ -4314,6 +4314,45 @@ int handler::ha_index_next_same(uchar *buf, const uchar *key, uint keylen) } +/** + Land on an approximately random row of the active index. + + Used by TABLESAMPLE SYSTEM (@see rr_sampling_system()), which needs to + visit a random sample of rows without reading every row like BERNOULLI + sampling does. The active index (opened with ha_index_init() by the + caller) is assumed to be the table's primary key, i.e. a full row is + available at every key value. The actual dive strategy is implemented + by the index_random_dive() virtual, which storage engines override. + + @param buf Buffer to read the found row into. + @param rand_state Random generator state to use for the dive. + + @retval 0 on success, otherwise the error returned by the underlying + index operation (including end-of-file style errors). +*/ + +int handler::ha_index_random_dive(uchar *buf, struct my_rnd_struct *rand_state) +{ + int result; + DBUG_ENTER("handler::ha_index_random_dive"); + DBUG_ASSERT(table_share->tmp_table != NO_TMP_TABLE || + m_lock_type != F_UNLCK); + DBUG_ASSERT(inited == INDEX); + + TABLE_IO_WAIT(tracker, PSI_TABLE_FETCH_ROW, active_index, result, + { result= index_random_dive(buf, rand_state); }) + increment_statistics(&SSV::ha_read_key_count); + if (!result) + { + update_index_statistics(); + if (table->vfield && buf == table->record[0]) + table->update_virtual_fields(this, VCOL_UPDATE_FOR_READ); + } + table->status= result ? STATUS_NOT_FOUND : 0; + DBUG_RETURN(result); +} + + bool handler::ha_was_semi_consistent_read() { bool result= was_semi_consistent_read(); diff --git a/sql/handler.h b/sql/handler.h index fa9196f189a8f..cc4daa5a9925f 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -4409,6 +4409,16 @@ class handler :public Sql_alloc virtual int index_last(uchar * buf) { return HA_ERR_WRONG_COMMAND; } virtual int index_next_same(uchar *buf, const uchar *key, uint keylen); + /** + @brief + Positions an index cursor to an approximately random row of the + active index and fetches it into buf. Storage engines that want to + support TABLESAMPLE SYSTEM must override this with an implementation + that exploits their own index structure (e.g. picking a random leaf + page). + */ + virtual int index_random_dive(uchar *buf, struct my_rnd_struct *rand_state) + { return HA_ERR_WRONG_COMMAND; } /** @brief The following functions works like index_read, but it find the last @@ -4447,6 +4457,7 @@ class handler :public Sql_alloc int ha_index_first(uchar * buf); int ha_index_last(uchar * buf); int ha_index_next_same(uchar *buf, const uchar *key, uint keylen); + int ha_index_random_dive(uchar *buf, struct my_rnd_struct *rand_state); /* TODO: should we make for those functions non-virtual ha_func_name wrappers, too? diff --git a/sql/records.cc b/sql/records.cc index 0e1d9021c2a16..5c613e6bbf710 100644 --- a/sql/records.cc +++ b/sql/records.cc @@ -31,6 +31,7 @@ #include "sql_base.h" #include "sql_sort.h" // SORT_ADDON_FIELD #include "sql_tablesample.h" +#include "key.h" // key_copy static int rr_quick(READ_RECORD *info); int rr_sequential(READ_RECORD *info); @@ -324,16 +325,33 @@ bool init_read_record(READ_RECORD *info,THD *thd, TABLE *table, else if (has_tablesample) { DBUG_PRINT("info",("using rr_sampling")); - Lex_tablesample *tablesample_clause= + Lex_tablesample *tablesample_clause= table->pos_in_table_list->tablesample_clause; enum tablesample_method_enum sampling_method= - tablesample_clause->get_sampling_method(); + tablesample_clause->get_effective_sampling_method(); if (sampling_method == tablesample_method_enum::TABLESAMPLE_BERNOULLI) + { info->read_record_func= rr_sampling_bernoulli; + if (unlikely(table->file->ha_rnd_init_with_error(1))) + DBUG_RETURN(1); + } else + { + int error; info->read_record_func= rr_sampling_system; - if (unlikely(table->file->ha_rnd_init_with_error(1))) - DBUG_RETURN(1); + info->sample_key= table->s->primary_key; + if (!table->file->inited && + unlikely((error= table->file->ha_index_init(info->sample_key, 0)))) + { + if (print_error) + table->file->print_error(error, MYF(0)); + DBUG_RETURN(1); + } + my_hash_init(PSI_INSTRUMENT_ME, &info->sample_seen_keys, + &my_charset_bin, 0, 0, + table->key_info[info->sample_key].key_length, + NULL, my_free, HASH_UNIQUE); + } tablesample_clause->seed_sample_rand(&info->sample_rand); } else @@ -368,6 +386,8 @@ void end_read_record(READ_RECORD *info) { /* free cache if used */ free_cache(info); + if (my_hash_inited(&info->sample_seen_keys)) + my_hash_free(&info->sample_seen_keys); if (info->table) { if (info->table->db_stat) // if opened @@ -559,19 +579,47 @@ int rr_sampling_bernoulli(READ_RECORD *info) int rr_sampling_system(READ_RECORD *info) { int tmp; - const double p= info->table->pos_in_table_list->tablesample_clause-> - get_sampling_percentage_fraction(); + handler *file= info->table->file; + KEY *key_info= &info->table->key_info[info->sample_key]; + const uint key_length= key_info->key_length; + + /* + used_stat_records was scaled down to sampling_fraction * total_rows + before + */ + if (info->sample_seen_keys.records >= info->table->stat_records()) + return rr_handle_error(info, HA_ERR_END_OF_FILE); for (;;) { - tmp= info->table->file->ha_rnd_next(info->record()); + tmp= file->ha_index_random_dive(info->record(), &info->sample_rand); if (tmp) { tmp= rr_handle_error(info, tmp); break; } - if (my_rnd(&info->sample_rand) < p) + + uchar *key_buff= (uchar*) my_malloc(PSI_INSTRUMENT_ME, key_length, + MYF(MY_WME)); + if (unlikely(!key_buff)) + { + tmp= HA_ERR_OUT_OF_MEM; break; + } + key_copy(key_buff, info->record(), key_info, key_length); + + if (my_hash_search(&info->sample_seen_keys, key_buff, key_length)) + { + my_free(key_buff); + continue; // already returned this row, dive again + } + + if (unlikely(my_hash_insert(&info->sample_seen_keys, key_buff))) + { + my_free(key_buff); + tmp= HA_ERR_OUT_OF_MEM; + } + break; } return tmp; } diff --git a/sql/records.h b/sql/records.h index dc7e167002297..22b1345911a37 100644 --- a/sql/records.h +++ b/sql/records.h @@ -16,6 +16,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ #include "table.h" +#include "hash.h" struct st_join_table; class handler; @@ -69,6 +70,15 @@ struct READ_RECORD // initialised only if dealing with tablesample clause struct my_rnd_struct sample_rand; + // initialised only when using rr_sampling_system (TABLESAMPLE SYSTEM) + uint sample_key; + /* + Primary key values of the rows already returned by rr_sampling_system(), + so that a later ha_index_random_dive() landing on a row we already + returned can be recognised and skipped instead of yielding a duplicate. + Initialised only when using rr_sampling_system (TABLESAMPLE SYSTEM). + */ + HASH sample_seen_keys; /* Structure storing information about sorting @@ -87,7 +97,7 @@ struct READ_RECORD Copy_field *copy_field; Copy_field *copy_field_end; public: - READ_RECORD() : table(NULL), cache(NULL) {} + READ_RECORD() : table(NULL), cache(NULL), sample_seen_keys() {} ~READ_RECORD() { end_read_record(this); } }; diff --git a/sql/share/errmsg-utf8.txt b/sql/share/errmsg-utf8.txt index 776df2a73477d..9394e2bf19f9b 100644 --- a/sql/share/errmsg-utf8.txt +++ b/sql/share/errmsg-utf8.txt @@ -12420,3 +12420,7 @@ ER_JSON_OPTIMIZER_REPLAY_CONTEXT_MATCH_FAILED eng "Failed to match the stats from replay context with the optimizer stats: %s doesn't exist in %s" ER_WRONG_SPVAR_TYPE_IN_TABLESAMPLE eng "A variable of a non-numeric based type in TABLESAMPLE clause" +ER_TABLESAMPLE_SYSTEM_NO_PK + eng "TABLESAMPLE SYSTEM requires a primary key on table '%-.192s'; using BERNOULLI sampling instead" +ER_TABLESAMPLE_SYSTEM_HIGH_PCT + eng "TABLESAMPLE SYSTEM on table '%-.192s' requested more than %g%% of rows; using BERNOULLI sampling instead" diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 170c1d4c9c0c2..74d8c2b206ff6 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -8560,7 +8560,7 @@ bool setup_tables(THD *thd, Name_resolution_context *context, if (table_list->tablesample_clause && (get_table_category( table_list->db, table_list->table_name) != TABLE_CATEGORY_USER || - table_list->tablesample_clause->fix_tablesample_fields(thd))) + table_list->tablesample_clause->fix_tablesample_fields(thd, table))) { my_error(ER_SYNTAX_ERROR, MYF(0)); DBUG_RETURN(1); @@ -8620,7 +8620,7 @@ bool setup_tables(THD *thd, Name_resolution_context *context, if (table_list->tablesample_clause && (get_table_category( table_list->db, table_list->table_name) != TABLE_CATEGORY_USER || - table_list->tablesample_clause->fix_tablesample_fields(thd))) + table_list->tablesample_clause->fix_tablesample_fields(thd, table))) { my_error(ER_SYNTAX_ERROR, MYF(0)); DBUG_RETURN(1); diff --git a/sql/sql_explain.cc b/sql/sql_explain.cc index 2e894deae5fce..bfb9d97d3ce87 100644 --- a/sql/sql_explain.cc +++ b/sql/sql_explain.cc @@ -1394,8 +1394,9 @@ void Explain_table_access::push_extra(enum explain_extra_tag extra_tag) void Explain_table_access::fill_key_str(String *key_str, bool is_json) const { CHARSET_INFO *cs= &my_charset_utf8mb4_bin; - bool is_hj= (type == JT_HASH || type == JT_HASH_NEXT || - type == JT_HASH_RANGE || type == JT_HASH_INDEX_MERGE); + bool is_hj= (type == JT_HASH || type == JT_HASH_NEXT || + type == JT_HASH_RANGE || type == JT_HASH_INDEX_MERGE || + type == JT_HASH_SAMPLE); LEX_CSTRING hash_key_prefix= { STRING_WITH_LEN("#hash#") }; const char *key_name; @@ -1441,8 +1442,9 @@ void Explain_table_access::fill_key_str(String *key_str, bool is_json) const void Explain_table_access::fill_key_len_str(String *key_len_str, bool is_json) const { - bool is_hj= (type == JT_HASH || type == JT_HASH_NEXT || - type == JT_HASH_RANGE || type == JT_HASH_INDEX_MERGE); + bool is_hj= (type == JT_HASH || type == JT_HASH_NEXT || + type == JT_HASH_RANGE || type == JT_HASH_INDEX_MERGE || + type == JT_HASH_SAMPLE); if (key.get_key_len() != (uint)-1) { char buf[64]; diff --git a/sql/sql_select.cc b/sql/sql_select.cc index cde3fda2bb3cb..fb399e8ec9a35 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -123,7 +123,8 @@ const char *join_type_str[]={ "UNKNOWN","system","const","eq_ref","ref", "MAYBE_REF","ALL","range","index","fulltext", "ref_or_null","unique_subquery","index_subquery", "index_merge", "hash_ALL", "hash_range", - "hash_index", "hash_index_merge" }; + "hash_index", "hash_index_merge", + "hash_sample", "sample" }; static const Lex_ident_column group_key= "group_key"_Lex_ident_column; static const Lex_ident_column distinct_key= "distinct_key"_Lex_ident_column; @@ -16930,34 +16931,33 @@ void JOIN_TAB::estimate_scan_time() } else { - sampling_info= table->pos_in_table_list->tablesample_clause; - if (unlikely(sampling_info && sampling_info->get_sampling_method() == + sampling_info= table->pos_in_table_list ? + table->pos_in_table_list->tablesample_clause : NULL; + if (unlikely(sampling_info && sampling_info->get_effective_sampling_method() == tablesample_method_enum::TABLESAMPLE_SYSTEM)) { + /* + get_effective_sampling_method() only returns TABLESAMPLE_SYSTEM + when the table has a primary key (otherwise it has already + fallen back to TABLESAMPLE_BERNOULLI), so table->s->primary_key + is guaranteed to be usable here. + */ cached_covering_key= table->s->primary_key; - if (cached_covering_key != MAX_KEY) + DBUG_ASSERT(cached_covering_key != MAX_KEY); + if (file->is_clustering_key(cached_covering_key)) { - if (file->is_clustering_key(cached_covering_key)) - { - cost->index_cost= - file->ha_keyread_clustered_time(cached_covering_key, records, records, 0); - read_time= file->cost(cost->index_cost); - row_copy_cost= file->ROW_COPY_COST; - } - else - { - cost->index_cost= - file->ha_keyread_time(cached_covering_key, records, records, 0); - cost->row_cost= file->ha_rnd_pos_time(records); - read_time= file->cost(cost->row_cost) + file->cost(cost->index_cost); - row_copy_cost= 0; // included in ha_rnd_pos_time - } + cost->index_cost= + file->ha_keyread_clustered_time(cached_covering_key, 1, records, 0); + read_time= file->cost(cost->index_cost); + row_copy_cost= file->ROW_COPY_COST; } else { - cost->row_cost= file->ha_scan_time(records); - read_time= file->cost(cost->row_cost); - row_copy_cost= 0; // Included in ha_scan_time + cost->index_cost= + file->ha_keyread_time(cached_covering_key, 1, records, 0); + cost->row_cost= file->ha_rnd_pos_time(records); + read_time= file->cost(cost->row_cost) + file->cost(cost->index_cost); + row_copy_cost= 0; // included in ha_rnd_pos_time } } else @@ -31306,6 +31306,8 @@ bool JOIN_TAB::save_explain_data(Explain_table_access *eta, else tab_type= type == JT_HASH ? JT_HASH_RANGE : JT_RANGE; } + else if (type == JT_HASH && table_list && table_list->tablesample_clause) + tab_type= JT_HASH_SAMPLE; eta->type= tab_type; /* Build "possible_keys" value */ diff --git a/sql/sql_select.h b/sql/sql_select.h index 48c07480152fd..d2ac3fd4570e5 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -435,8 +435,18 @@ enum join_type Shown as "hash_index_merge" in EXPLAIN. */ JT_HASH_INDEX_MERGE, + + /* + JT_HASH where the joined table also carries a TABLESAMPLE clause, + so the buffer-refill scan reads a sample of the table's rows + instead of all of them. Appears only in EXPLAIN output, derived + from a JT_HASH table whose table has a tablesample_clause; never + the type of a JOIN_TAB. + + Shown as "hash_sample" in EXPLAIN. + */ + JT_HASH_SAMPLE, JT_SAMPLE, - JT_HASH_SAMPLE }; class JOIN; diff --git a/sql/sql_tablesample.cc b/sql/sql_tablesample.cc index b4e3c760ac6a1..e3ca741bd2933 100644 --- a/sql/sql_tablesample.cc +++ b/sql/sql_tablesample.cc @@ -15,24 +15,49 @@ #include "sql_tablesample.h" #include "sql_class.h" +#include "table.h" +#include "sql_error.h" -int Lex_tablesample::fix_tablesample_fields(THD *thd) +int Lex_tablesample::fix_tablesample_fields(THD *thd, TABLE *table) { -DBUG_ENTER("Lex_tablesample::fix_tablesample_fields"); -DBUG_ASSERT(thd); -bool err= sampling_percentage->fix_fields_if_needed(thd, NULL); -if(err) - DBUG_RETURN(1); -if (sampling_percentage->const_item()) -{ - double d= sampling_percentage->val_real(); - if (d < 0.0 || d > 100.0) - DBUG_RETURN(1); - percentage= d / 100.0; + DBUG_ENTER("Lex_tablesample::fix_tablesample_fields"); + DBUG_ASSERT(thd); + DBUG_ASSERT(table); + bool err= sampling_percentage->fix_fields_if_needed(thd, NULL); + if(err) + DBUG_RETURN(1); + if (sampling_percentage->const_item()) + { + double d= sampling_percentage->val_real(); + if (d < 0.0 || d > 100.0) + DBUG_RETURN(1); + percentage= d / 100.0; + + seed1= (ulong)(my_rnd(&thd->rand) * (double) 0xFFFFFFFFUL); + seed2= (ulong)(my_rnd(&thd->rand) * (double) 0xFFFFFFFFUL); + } - seed1= (ulong)(my_rnd(&thd->rand) * (double) 0xFFFFFFFFUL); - seed2= (ulong)(my_rnd(&thd->rand) * (double) 0xFFFFFFFFUL); -} + effective_method= sampling_method; + if (sampling_method == TABLESAMPLE_SYSTEM) + { + if (table->s->primary_key == MAX_KEY) + { + effective_method= TABLESAMPLE_BERNOULLI; + push_warning_printf(thd, Sql_condition::WARN_LEVEL_NOTE, + ER_TABLESAMPLE_SYSTEM_NO_PK, + ER_THD(thd, ER_TABLESAMPLE_SYSTEM_NO_PK), + table->alias.c_ptr()); + } + else if (percentage > SYSTEM_TO_BERNOULLI_MAX_FRACTION) + { + effective_method= TABLESAMPLE_BERNOULLI; + push_warning_printf(thd, Sql_condition::WARN_LEVEL_NOTE, + ER_TABLESAMPLE_SYSTEM_HIGH_PCT, + ER_THD(thd, ER_TABLESAMPLE_SYSTEM_HIGH_PCT), + table->alias.c_ptr(), + SYSTEM_TO_BERNOULLI_MAX_FRACTION * 100.0); + } + } -DBUG_RETURN(0); + DBUG_RETURN(0); } \ No newline at end of file diff --git a/sql/sql_tablesample.h b/sql/sql_tablesample.h index 5ca1c76f6c866..cdaa016e18b27 100644 --- a/sql/sql_tablesample.h +++ b/sql/sql_tablesample.h @@ -18,8 +18,8 @@ #include "my_global.h" -#include "item.h" #include "sql_alloc.h" +#include "my_rnd.h" enum tablesample_method_enum { @@ -29,12 +29,36 @@ enum tablesample_method_enum }; class THD; +struct TABLE; +class Item; class Lex_tablesample: public Sql_alloc { private: + /* + SYSTEM sampling draws independent random index descents and rejects + duplicates (see mi_random_dive() / rr_sampling_system()). As the + requested fraction approaches 1, collisions between draws become + frequent, and the expected number of descents needed to collect p*N + distinct rows grows like N * ln(1/(1-p)) -- unboundedly worse than a + single sequential scan as p -> 1. Past this fraction, BERNOULLI + (cost O(N), independent of p) is cheaper, so SYSTEM falls back to it. + */ + static constexpr double SYSTEM_TO_BERNOULLI_MAX_FRACTION= 0.3; + enum tablesample_method_enum sampling_method= tablesample_method_enum::TABLESAMPLE_NONE; + /* + The method actually used at read/cost time, which may differ from + sampling_method (e.g. SYSTEM falls back to BERNOULLI when the table + has no primary key). Recomputed on every fix_tablesample_fields() call + (once per statement execution) from the immutable sampling_method, so + that a prepared statement/stored procedure re-executed against a table + whose primary key changed always reflects the current table, never a + stale decision from an earlier execution. + */ + enum tablesample_method_enum effective_method= + tablesample_method_enum::TABLESAMPLE_NONE; Item *sampling_percentage; double percentage= 0.0; ulong seed1= 0; @@ -42,9 +66,10 @@ class Lex_tablesample: public Sql_alloc public: Lex_tablesample(enum tablesample_method_enum method, Item *percentage) : - sampling_method(method), sampling_percentage(percentage) {} + sampling_method(method), effective_method(method), + sampling_percentage(percentage) {} - int fix_tablesample_fields(THD *thd); + int fix_tablesample_fields(THD *thd, TABLE *table); double get_sampling_percentage_fraction() const { @@ -56,6 +81,11 @@ class Lex_tablesample: public Sql_alloc return sampling_method; } + tablesample_method_enum get_effective_sampling_method() const + { + return effective_method; + } + void seed_sample_rand(my_rnd_struct *out) const { my_rnd_init(out, seed1, seed2); diff --git a/storage/myisam/CMakeLists.txt b/storage/myisam/CMakeLists.txt index 2f5d6211e366a..bb626c8fbcfd2 100644 --- a/storage/myisam/CMakeLists.txt +++ b/storage/myisam/CMakeLists.txt @@ -19,8 +19,9 @@ SET(MYISAM_SOURCES ft_boolean_search.c ft_nlq_search.c ft_parser.c ft_static.c ft_stopwords.c ft_update.c mi_cache.c mi_changed.c mi_check.c mi_checksum.c mi_close.c mi_create.c mi_dbug.c mi_delete.c mi_delete_all.c mi_delete_table.c mi_dynrec.c mi_extra.c mi_info.c - mi_key.c mi_keycache.c mi_locking.c mi_log.c mi_open.c - mi_packrec.c mi_page.c mi_panic.c mi_preload.c mi_range.c mi_rename.c + mi_key.c mi_keycache.c mi_locking.c mi_log.c mi_open.c + mi_packrec.c mi_page.c mi_panic.c mi_preload.c mi_random_dive.c + mi_range.c mi_rename.c mi_rfirst.c mi_rlast.c mi_rnext.c mi_rnext_same.c mi_rprev.c mi_rrnd.c mi_rsame.c mi_rsamepos.c mi_scan.c mi_search.c mi_static.c mi_statrec.c mi_unique.c mi_update.c mi_write.c rt_index.c rt_key.c rt_mbr.c diff --git a/storage/myisam/ha_myisam.cc b/storage/myisam/ha_myisam.cc index 7904b7193ae00..b62403aae0adf 100644 --- a/storage/myisam/ha_myisam.cc +++ b/storage/myisam/ha_myisam.cc @@ -2047,6 +2047,12 @@ int ha_myisam::index_next_same(uchar *buf, return error; } +int ha_myisam::index_random_dive(uchar *buf, struct my_rnd_struct *rand_state) +{ + DBUG_ASSERT(inited==INDEX); + return mi_random_dive(file, active_index, buf, rand_state); +} + int ha_myisam::rnd_init(bool scan) { diff --git a/storage/myisam/ha_myisam.h b/storage/myisam/ha_myisam.h index 0bfdc2d4b57a2..067d4888b7571 100644 --- a/storage/myisam/ha_myisam.h +++ b/storage/myisam/ha_myisam.h @@ -76,6 +76,7 @@ class ha_myisam final : public handler int index_first(uchar * buf) override; int index_last(uchar * buf) override; int index_next_same(uchar *buf, const uchar *key, uint keylen) override; + int index_random_dive(uchar *buf, struct my_rnd_struct *rand_state) override; int ft_init() override { if (!ft_handler) diff --git a/storage/myisam/mi_random_dive.c b/storage/myisam/mi_random_dive.c new file mode 100644 index 0000000000000..aa35cbac84289 --- /dev/null +++ b/storage/myisam/mi_random_dive.c @@ -0,0 +1,110 @@ +/* Copyright (c) 2026 Dearsh Oberoi + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#include "myisamdef.h" +#include + +int mi_random_dive(MI_INFO *info, int inx, uchar *buf, + struct my_rnd_struct *rand_state) +{ + MI_KEYDEF *keyinfo; + my_off_t pos; + uchar *buff, *page, *end, t_buff[HA_MAX_KEY_BUFF]; + uint nod_flag, max_keynr, total_slots, chosen_slot, cur_slot; + DBUG_ENTER("mi_random_dive"); + + if ((inx= _mi_check_index(info,inx)) < 0) + DBUG_RETURN(my_errno); + + if (fast_mi_readinfo(info)) + DBUG_RETURN(my_errno); + + keyinfo=info->s->keyinfo+inx; + if (keyinfo->key_alg != HA_KEY_ALG_BTREE) + { + my_errno= HA_ERR_WRONG_COMMAND; + DBUG_RETURN(my_errno); + } + + if (info->s->concurrent_insert) + mysql_rwlock_rdlock(&info->s->key_root_lock[inx]); + + pos= info->s->state.key_root[inx]; + + for(;;) + { + if (!(buff=_mi_fetch_keypage(info,keyinfo,pos,DFLT_INIT_HITS,info->buff,1))) + goto err; + + nod_flag=mi_test_if_nod(buff); + page= buff+2+nod_flag; + end= buff+mi_getint(buff); + t_buff[0]= 0; + max_keynr= 0; + + while (page < end) + { + if (!(*keyinfo->get_key)(keyinfo,nod_flag,&page,t_buff)) + goto err; + max_keynr++; + } + + total_slots= max_keynr+(nod_flag > 0 ? 1 : 0); + if (!total_slots) + { + my_errno= HA_ERR_END_OF_FILE; + goto err; + } + chosen_slot= (uint) (total_slots*my_rnd(rand_state)); + if (chosen_slot >= total_slots) + chosen_slot--; + + page= buff+2+nod_flag; + t_buff[0]= 0; + cur_slot= 0; + while (page < end && cur_slot < chosen_slot + (nod_flag ? 0 : 1)) + { + if (!(*keyinfo->get_key)(keyinfo,nod_flag,&page,t_buff)) + goto err; + cur_slot++; + } + + if (nod_flag) + { + pos= _mi_kpos(nod_flag, page); + } else { + pos= _mi_dpos(info, nod_flag, page); + break; + } + } + + info->lastpos= pos; + + if (info->s->concurrent_insert) + mysql_rwlock_unlock(&info->s->key_root_lock[inx]); + + if (!(*info->read_record)(info,info->lastpos,buf)) + { + info->update|= HA_STATE_AKTIV; + DBUG_RETURN(0); + } + + DBUG_RETURN(my_errno); + +err: + if (info->s->concurrent_insert) + mysql_rwlock_unlock(&info->s->key_root_lock[inx]); + DBUG_RETURN(my_errno); +} \ No newline at end of file From 698e1582cd065c410a8c987d1c4a457fb81cb100 Mon Sep 17 00:00:00 2001 From: deo002 Date: Sat, 22 Aug 2026 22:39:37 +0530 Subject: [PATCH 6/7] MDEV-38992 Fix use-after-free crash in acl_init reading TABLE::pos_in_table_list Grant_tables::open_and_lock() frees its local TABLE_LIST array (my_free(tables)) once the grant tables are open, but each opened TABLE::pos_in_table_list was left pointing into that now-freed memory. This dangling pointer was previously never dereferenced, but init_read_record()'s new TABLESAMPLE check reads table->pos_in_table_list->tablesample_clause unconditionally leading to a crash Signed-off-by: deo002 --- sql/sql_acl.cc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index 450fa893f32a8..d63cb61ed9b16 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -2750,7 +2750,15 @@ class Grant_tables DBUG_RETURN(res); if (build_table_list(thd, &first, which_tables, lock_type, tables)) - goto func_exit; + { + /* + No table in 'tables' has been touched yet (all entries are still + uninitialized memory from the my_malloc() above), so we must not + run the pos_in_table_list cleanup loop in func_exit against it. + */ + my_free(tables); + DBUG_RETURN(res); + } res= really_open(thd, first, &counter); @@ -2802,6 +2810,9 @@ class Grant_tables } func_exit: + for (int i= USER_TABLE; i >= 0; i--) + if (tables[i].table) + tables[i].table->pos_in_table_list= NULL; my_free(tables); DBUG_RETURN(res); } From 9a6d605f30c7cd198ef31c2625c0774abea5ed2d Mon Sep 17 00:00:00 2001 From: deo002 Date: Sun, 23 Aug 2026 22:32:29 +0530 Subject: [PATCH 7/7] MDEV-38992 Update perfschema digest hash expected results for new TABLESAMPLE tokens The new TABLESAMPLE grammar tokens shifted generated token IDs used by the statement digest hasher, so every digest hash changed. Signed-off-by: deo002 --- libmysqld/CMakeLists.txt | 1 + mysql-test/main/tablesample.result | 16 +++++- mysql-test/main/tablesample.test | 27 +++++++++- .../suite/perfschema/r/digest_view.result | 50 +++++++++---------- .../start_server_low_digest_sql_length.result | 4 +- sql/opt_hints.cc | 6 --- sql/sql_prepare.cc | 2 +- sql/sql_tablesample.cc | 4 +- sql/sql_tablesample.h | 5 -- sql/table.cc | 6 --- 10 files changed, 72 insertions(+), 49 deletions(-) diff --git a/libmysqld/CMakeLists.txt b/libmysqld/CMakeLists.txt index d38f2a48c737b..734a910cac992 100644 --- a/libmysqld/CMakeLists.txt +++ b/libmysqld/CMakeLists.txt @@ -106,6 +106,7 @@ SET(SQL_EMBEDDED_SOURCES emb_qcache.cc libmysqld.c lib_sql.cc ../sql/sql_show.cc ../sql/sql_state.c ../sql/sql_statistics.cc ../sql/sql_string.cc ../sql/sql_table.cc ../sql/sql_test.cc + ../sql/sql_tablesample.cc ../sql/ddl_log.cc ../sql/item_vectorfunc.cc ../sql/sql_trigger.cc ../sql/sql_udf.cc ../sql/sql_union.cc ../sql/sql_update.cc ../sql/sql_view.cc ../sql/sql_profile.cc diff --git a/mysql-test/main/tablesample.result b/mysql-test/main/tablesample.result index 7ccf06fc75843..1dd07e4403ac0 100644 --- a/mysql-test/main/tablesample.result +++ b/mysql-test/main/tablesample.result @@ -28,6 +28,9 @@ a b SET @pct = 300; EXECUTE stmt2 USING @pct; ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +SET @pct = NULL; +EXECUTE stmt2 USING @pct; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use DEALLOCATE PREPARE stmt2; DROP PROCEDURE IF EXISTS p1; Warnings: @@ -66,6 +69,17 @@ BEGIN SELECT * FROM t1 TABLESAMPLE BERNOULLI (sample_pct); END// ERROR HY000: A variable of a non-numeric based type in TABLESAMPLE clause +DROP PROCEDURE IF EXISTS p1; +Warnings: +Note 1305 PROCEDURE test.p1 does not exist +CREATE PROCEDURE p1() +BEGIN +DECLARE c SYS_REFCURSOR; +OPEN c FOR 'SELECT * FROM t1 TABLESAMPLE BERNOULLI (?)' USING NULL; +END// +CALL p1(); +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +DROP PROCEDURE p1; SELECT * FROM information_schema.tables TABLESAMPLE BERNOULLI (5); ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use SELECT * FROM mysql.user TABLESAMPLE SYSTEM (10); @@ -133,5 +147,5 @@ id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE t1 sample NULL NULL NULL NULL 4 1 SIMPLE t2 hash_sample NULL #hash#$hj: 4: test.t1.a 6 Using where; Using join buffer (flat, BNLH join) SET join_cache_level= @save_join_cache_level; -DROP TABLE t1, t2; +DROP TABLE t1, t2, t_data; DROP VIEW v1; diff --git a/mysql-test/main/tablesample.test b/mysql-test/main/tablesample.test index b76359cd4124a..caaae50a7962f 100644 --- a/mysql-test/main/tablesample.test +++ b/mysql-test/main/tablesample.test @@ -4,6 +4,12 @@ # For now, there are tests just for checking syntax # +# TABLESAMPLE is not allowed on views, and the view-protocol's +# CREATE VIEW/SELECT/DROP VIEW rewriting of plain SELECTs clears the +# session's warning list between statements, breaking the SHOW WARNINGS +# checks below. +--source include/no_view_protocol.inc + --disable_warnings DROP TABLE IF EXISTS t1, t2; DROP VIEW IF EXISTS v1; @@ -48,6 +54,10 @@ SET @pct = 300; --error ER_SYNTAX_ERROR EXECUTE stmt2 USING @pct; +SET @pct = NULL; +--error ER_SYNTAX_ERROR +EXECUTE stmt2 USING @pct; + DEALLOCATE PREPARE stmt2; # @@ -100,6 +110,21 @@ BEGIN END// DELIMITER ;// +DROP PROCEDURE IF EXISTS p1; + +DELIMITER //; +CREATE PROCEDURE p1() +BEGIN + DECLARE c SYS_REFCURSOR; + OPEN c FOR 'SELECT * FROM t1 TABLESAMPLE BERNOULLI (?)' USING NULL; +END// +DELIMITER ;// + +--error ER_SYNTAX_ERROR +CALL p1(); + +DROP PROCEDURE p1; + # # TABLESAMPLE should not work on system tables # @@ -192,5 +217,5 @@ SET join_cache_level= @save_join_cache_level; # # Cleanup # -DROP TABLE t1, t2; +DROP TABLE t1, t2, t_data; DROP VIEW v1; \ No newline at end of file diff --git a/mysql-test/suite/perfschema/r/digest_view.result b/mysql-test/suite/perfschema/r/digest_view.result index 346b5e0c44e60..8e35daac2ae79 100644 --- a/mysql-test/suite/perfschema/r/digest_view.result +++ b/mysql-test/suite/perfschema/r/digest_view.result @@ -191,17 +191,17 @@ SELECT SCHEMA_NAME, DIGEST, DIGEST_TEXT, COUNT_STAR FROM performance_schema.events_statements_summary_by_digest ORDER BY DIGEST_TEXT; SCHEMA_NAME DIGEST DIGEST_TEXT COUNT_STAR -test cc5e38c5a702f49627052e59a2603818 EXPLAIN SELECT * FROM `test` . `v1` 1 -test 264b69debfd30bbfe374cdca018fa4f9 EXPLAIN SELECT * FROM `test` . `v1` WHERE `a` = ? 1 -test 75f31bf60b75a4f851ff9c9ee4e19d96 EXPLAIN SELECT * FROM `test` . `v1` WHERE `b` > ? 1 -test a8f8a85697afacda9f2f3d3b023f9ed0 EXPLAIN SELECT `a` , `b` FROM `test` . `v1` 1 -test 512598992d37826136f3f1292bb32e1e EXPLAIN SELECT `b` , `a` FROM `test` . `v1` 1 -test 54342cc19df16ce54b3ba43d1fd7552d SELECT * FROM `test` . `v1` 1 -test 3750b7d8c33e040b90a2cdccb4c642a3 SELECT * FROM `test` . `v1` WHERE `a` = ? 1 -test 94af70ef76a31364845f534863d99da8 SELECT * FROM `test` . `v1` WHERE `b` > ? 1 -test 0c87d86b62e664a81a23d4f176fbb377 SELECT `a` , `b` FROM `test` . `v1` 1 -test de7e6f1350ff14a952b97b373d76c1a4 SELECT `b` , `a` FROM `test` . `v1` 1 -test 3468091e6d6ce474aded20beaec36b53 TRUNCATE TABLE `performance_schema` . `events_statements_summary_by_digest` 1 +test c699db3b539c46397d75248202bfa2c0 EXPLAIN SELECT * FROM `test` . `v1` 1 +test 0533091953f8f00b28ba736031fbd34a EXPLAIN SELECT * FROM `test` . `v1` WHERE `a` = ? 1 +test 9d01d8279d3efdd1ca19525ab81867f5 EXPLAIN SELECT * FROM `test` . `v1` WHERE `b` > ? 1 +test e738c5785ebf35d0dc3294c57cac56b2 EXPLAIN SELECT `a` , `b` FROM `test` . `v1` 1 +test e529550d920a19bd9af0ff7b8be014db EXPLAIN SELECT `b` , `a` FROM `test` . `v1` 1 +test 8f07567d81b04a27905f93f4d21ae704 SELECT * FROM `test` . `v1` 1 +test 18892865d73c2762bf2116f65e2783f5 SELECT * FROM `test` . `v1` WHERE `a` = ? 1 +test 9e5974b88fe2d62ce119b11a438e6af9 SELECT * FROM `test` . `v1` WHERE `b` > ? 1 +test 895a6385ee5c0386780deec58dad43a6 SELECT `a` , `b` FROM `test` . `v1` 1 +test b25031d48f079aa480fd6c84baf8eb2b SELECT `b` , `a` FROM `test` . `v1` 1 +test 76be74b215fee3701da4d982dd87cd48 TRUNCATE TABLE `performance_schema` . `events_statements_summary_by_digest` 1 DROP TABLE test.v1; CREATE VIEW test.v1 AS SELECT * FROM test.t1; EXPLAIN SELECT * from test.v1; @@ -248,19 +248,19 @@ SELECT SCHEMA_NAME, DIGEST, DIGEST_TEXT, COUNT_STAR FROM performance_schema.events_statements_summary_by_digest ORDER BY DIGEST_TEXT; SCHEMA_NAME DIGEST DIGEST_TEXT COUNT_STAR -test ef8b10f452e117fa96af49687a93a10f CREATE VIEW `test` . `v1` AS SELECT * FROM `test` . `t1` 1 -test 6b99f4d2ad410f9fa4ee1d501b0db571 DROP TABLE `test` . `v1` 1 -test cc5e38c5a702f49627052e59a2603818 EXPLAIN SELECT * FROM `test` . `v1` 2 -test 264b69debfd30bbfe374cdca018fa4f9 EXPLAIN SELECT * FROM `test` . `v1` WHERE `a` = ? 2 -test 75f31bf60b75a4f851ff9c9ee4e19d96 EXPLAIN SELECT * FROM `test` . `v1` WHERE `b` > ? 2 -test a8f8a85697afacda9f2f3d3b023f9ed0 EXPLAIN SELECT `a` , `b` FROM `test` . `v1` 2 -test 512598992d37826136f3f1292bb32e1e EXPLAIN SELECT `b` , `a` FROM `test` . `v1` 2 -test 54342cc19df16ce54b3ba43d1fd7552d SELECT * FROM `test` . `v1` 2 -test 3750b7d8c33e040b90a2cdccb4c642a3 SELECT * FROM `test` . `v1` WHERE `a` = ? 2 -test 94af70ef76a31364845f534863d99da8 SELECT * FROM `test` . `v1` WHERE `b` > ? 2 -test 187a846fafe04b746eddaaab80b6a766 SELECT SCHEMA_NAME , `DIGEST` , `DIGEST_TEXT` , `COUNT_STAR` FROM `performance_schema` . `events_statements_summary_by_digest` ORDER BY `DIGEST_TEXT` 1 -test 0c87d86b62e664a81a23d4f176fbb377 SELECT `a` , `b` FROM `test` . `v1` 2 -test de7e6f1350ff14a952b97b373d76c1a4 SELECT `b` , `a` FROM `test` . `v1` 2 -test 3468091e6d6ce474aded20beaec36b53 TRUNCATE TABLE `performance_schema` . `events_statements_summary_by_digest` 1 +test f852e38b9370fbfe50ceb58f1b214695 CREATE VIEW `test` . `v1` AS SELECT * FROM `test` . `t1` 1 +test d61fc37b0d8796fa1760a9d0ffc1ca4e DROP TABLE `test` . `v1` 1 +test c699db3b539c46397d75248202bfa2c0 EXPLAIN SELECT * FROM `test` . `v1` 2 +test 0533091953f8f00b28ba736031fbd34a EXPLAIN SELECT * FROM `test` . `v1` WHERE `a` = ? 2 +test 9d01d8279d3efdd1ca19525ab81867f5 EXPLAIN SELECT * FROM `test` . `v1` WHERE `b` > ? 2 +test e738c5785ebf35d0dc3294c57cac56b2 EXPLAIN SELECT `a` , `b` FROM `test` . `v1` 2 +test e529550d920a19bd9af0ff7b8be014db EXPLAIN SELECT `b` , `a` FROM `test` . `v1` 2 +test 8f07567d81b04a27905f93f4d21ae704 SELECT * FROM `test` . `v1` 2 +test 18892865d73c2762bf2116f65e2783f5 SELECT * FROM `test` . `v1` WHERE `a` = ? 2 +test 9e5974b88fe2d62ce119b11a438e6af9 SELECT * FROM `test` . `v1` WHERE `b` > ? 2 +test 7fc6b5fae4fbeb78bccd2f9771c67075 SELECT SCHEMA_NAME , `DIGEST` , `DIGEST_TEXT` , `COUNT_STAR` FROM `performance_schema` . `events_statements_summary_by_digest` ORDER BY `DIGEST_TEXT` 1 +test 895a6385ee5c0386780deec58dad43a6 SELECT `a` , `b` FROM `test` . `v1` 2 +test b25031d48f079aa480fd6c84baf8eb2b SELECT `b` , `a` FROM `test` . `v1` 2 +test 76be74b215fee3701da4d982dd87cd48 TRUNCATE TABLE `performance_schema` . `events_statements_summary_by_digest` 1 DROP VIEW test.v1; DROP TABLE test.t1; diff --git a/mysql-test/suite/perfschema/r/start_server_low_digest_sql_length.result b/mysql-test/suite/perfschema/r/start_server_low_digest_sql_length.result index e4e2c8b294a3f..975f52df7c3bf 100644 --- a/mysql-test/suite/perfschema/r/start_server_low_digest_sql_length.result +++ b/mysql-test/suite/perfschema/r/start_server_low_digest_sql_length.result @@ -8,5 +8,5 @@ SELECT 1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1 #################################### SELECT event_name, digest, digest_text, sql_text FROM events_statements_history_long; event_name digest digest_text sql_text -statement/sql/select c9e142fe40c43498607ca5310e11d2ce SELECT ? + ? + SELECT ... -statement/sql/truncate 506e3496d92689cd2367329dc7725165 TRUNCATE TABLE truncat... +statement/sql/select 6e58e4950cd512b73ca69386ac9b919a SELECT ? + ? + SELECT ... +statement/sql/truncate fbc57747ac49e51b80f0f72177d8ff52 TRUNCATE TABLE truncat... diff --git a/sql/opt_hints.cc b/sql/opt_hints.cc index a1fe754265c47..73a5d3e62c9c6 100644 --- a/sql/opt_hints.cc +++ b/sql/opt_hints.cc @@ -958,12 +958,6 @@ void Opt_hints_table::update_index_hint_map(Key_map *keys_to_use, bool Opt_hints_table::update_index_hint_maps(THD *thd, TABLE *tbl) { - /* - A TABLESAMPLE clause forces a sampling scan of the table and - index-based access can bias the result. Ignore any index hints - (old- or new-style) entirely and make sure no key is considered - usable, regardless of what the hints say. - */ if (tbl->pos_in_table_list && tbl->pos_in_table_list->tablesample_clause) { tbl->keys_in_use_for_query.clear_all(); diff --git a/sql/sql_prepare.cc b/sql/sql_prepare.cc index ca503687e16a2..db433c31fff11 100644 --- a/sql/sql_prepare.cc +++ b/sql/sql_prepare.cc @@ -296,7 +296,7 @@ class Prepared_statement: public Statement } if (param->tablesample_clause_param && !param->has_double_value()) { - if (param->set_tablesample_clause_param(param->val_real())) + if (!param->is_null() && param->set_tablesample_clause_param(param->val_real())) DBUG_RETURN(true); } } diff --git a/sql/sql_tablesample.cc b/sql/sql_tablesample.cc index e3ca741bd2933..646288c129796 100644 --- a/sql/sql_tablesample.cc +++ b/sql/sql_tablesample.cc @@ -23,13 +23,13 @@ int Lex_tablesample::fix_tablesample_fields(THD *thd, TABLE *table) DBUG_ENTER("Lex_tablesample::fix_tablesample_fields"); DBUG_ASSERT(thd); DBUG_ASSERT(table); - bool err= sampling_percentage->fix_fields_if_needed(thd, NULL); + bool err= sampling_percentage->fix_fields_if_needed(thd, &sampling_percentage); if(err) DBUG_RETURN(1); if (sampling_percentage->const_item()) { double d= sampling_percentage->val_real(); - if (d < 0.0 || d > 100.0) + if (sampling_percentage->null_value || d < 0.0 || d > 100.0) DBUG_RETURN(1); percentage= d / 100.0; diff --git a/sql/sql_tablesample.h b/sql/sql_tablesample.h index cdaa016e18b27..d703d4b0d7a4d 100644 --- a/sql/sql_tablesample.h +++ b/sql/sql_tablesample.h @@ -76,11 +76,6 @@ class Lex_tablesample: public Sql_alloc return percentage; } - tablesample_method_enum get_sampling_method() const - { - return sampling_method; - } - tablesample_method_enum get_effective_sampling_method() const { return effective_method; diff --git a/sql/table.cc b/sql/table.cc index 2b3c1515876e2..dedfa451706e4 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -9084,12 +9084,6 @@ Item_subselect *TABLE_LIST::containing_subselect() */ bool TABLE_LIST::process_index_hints(TABLE *tbl) { - /* - A TABLESAMPLE clause forces a sampling scan of the table and - index-based access can bias the result. Ignore any index hints - (old- or new-style) entirely and make sure no key is considered - usable, regardless of what the hints say. - */ if (tbl->pos_in_table_list && tbl->pos_in_table_list->tablesample_clause) { tbl->keys_in_use_for_query.clear_all();