From 0183be8acb1102a3ae8fc7357c4dc2b1678f9e4d Mon Sep 17 00:00:00 2001 From: deo002 Date: Tue, 9 Jun 2026 23:52:39 +0530 Subject: [PATCH 1/4] 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 b05a7d5665cab..2b49363e04a9c 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 a9babd0ea928a..c0d74d3c67fd4 100644 --- a/sql/share/errmsg-utf8.txt +++ b/sql/share/errmsg-utf8.txt @@ -12414,3 +12414,5 @@ ER_PARTITION_INTERVAL_MAXVALUE eng "MAXVALUE is not allowed in range partitioning with interval" ER_RANGE_INTERVAL_PART_FAILED eng "Range partition table %`s.%`s: adding INTERVAL partition(s) failed" +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 fc818b465fd00..2e408f003bcfe 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 bd83d7f3def98..7b7b7bccfb622 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -9516,7 +9516,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()); @@ -9546,15 +9546,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) { @@ -9578,12 +9599,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 a63a6f376031d..ed72add592dfb 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 }; @@ -4491,6 +4497,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 @@ -4517,6 +4551,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 1c465f3ea219a..9a32daa4d7ebc 100644 --- a/sql/sql_string.cc +++ b/sql/sql_string.cc @@ -584,6 +584,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 d6a0257ad9bb6..005765010d908 100644 --- a/sql/sql_string.h +++ b/sql/sql_string.h @@ -1060,6 +1060,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 65d9378d5a221..12cf77aa1d6fd 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 @@ -12851,7 +12860,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, @@ -12862,6 +12871,8 @@ table_primary_ident: MYSQL_YYABORT; if ($3) $$->vers_conditions= Lex->vers_conditions; + if ($6) + $$->tablesample_clause= $6; } ; @@ -13070,6 +13081,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 8521fc85ef9c37162d4ebac9470a37bf5ecbc619 Mon Sep 17 00:00:00 2001 From: deo002 Date: Sun, 5 Jul 2026 21:43:13 +0530 Subject: [PATCH 2/4] 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 7e8167eaaf7e5bce832d3627bb4777a4abc29415 Mon Sep 17 00:00:00 2001 From: deo002 Date: Thu, 9 Jul 2026 22:07:19 +0530 Subject: [PATCH 3/4] 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 8e78bc9f994cf..b4635df04cbb2 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -70,6 +70,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. @@ -16874,6 +16875,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; @@ -16906,9 +16908,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 d621f377afd72..39e12d23c251a 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 @@ -4155,6 +4156,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 d510b4fb67d8afd2e5fd71a554190fdac7281edc Mon Sep 17 00:00:00 2001 From: deo002 Date: Sun, 9 Aug 2026 14:49:49 +0530 Subject: [PATCH 4/4] 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 2665778d5ff97..330b190a7d926 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 b4635df04cbb2..55dbb4a85a837 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -3458,6 +3458,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)) { @@ -8803,6 +8804,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"); @@ -9936,14 +9939,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; @@ -10023,7 +10026,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); @@ -13453,6 +13457,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) @@ -13474,7 +13480,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; @@ -16067,6 +16073,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) @@ -16158,7 +16165,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; @@ -16241,6 +16249,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, @@ -16527,6 +16536,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; /* @@ -17040,7 +17050,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