Skip to content

MDEV-38992 TABLESAMPLE Clause Implementation - #5208

Open
deo002 wants to merge 7 commits into
MariaDB:mainfrom
deo002:feat/tablesample
Open

MDEV-38992 TABLESAMPLE Clause Implementation#5208
deo002 wants to merge 7 commits into
MariaDB:mainfrom
deo002:feat/tablesample

Conversation

@deo002

@deo002 deo002 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Initial draft of the TABLESAMPLE (MDEV-38992) clause implementation

Description

In a table reference, an optional tablesample clause can be specified to return a subset of random rows depending on the sampling method and sampling percentage.

Syntax:

<table factor> ::= <table primary> [ <sample clause> ]
<sample clause> ::= TABLESAMPLE <sample method> <left paren> <sample percentage> <right paren>
<sample method> ::= BERNOULLI | SYSTEM
<sample percentage> ::= <numeric value expression>

There are two sampling methods:

  • Bernoulli
    In this, the entire table is scanned and for each row a random number from 0 to 1 is generated, say x. If x's value is less than the sampling percentage p, then the row is added to the result row set.

  • System(for MyISAM engine)
    In this, rows are selected by performing a random descent into the b-tree of a primary key.
    The random dive algorithm works in the following way:

    • Start at root.
    • See how many child pointers are in the current node, say N. Generate a random number between 0 and N-1, say i. Pick a child pointer indexed at i and descend into the child.
    • Repeat step 2 until we reach a leaf node pointing to a record. This will be our random row.

    An alternative reverse records_in_range approach was also suggested in the Jira ticket. The random dive approach seemed easier to implement and the result was same, so went ahead with it.
    However, the random descent approach, like the reverse records_in_range approach is also skewed. It does not consider the sub-tree size when choosing a random child pointer from a node. Child pointers pointing to bigger sub-trees must have more weight. This can be fixed with some pre-computation with a full tree traversal but doesn't seem worth pursuing because of the IO overhead.

    As the random dive logic can stumble upon rows it had already returned, a hash set is used to avoid deduplication.

    System sampling is much faster for smaller sampling percentages when compared to Bernoulli sampling.

Implementation Details

Query Parsing

  • Defined a class Lex_tablesample to store all the sampling method and percentage (sql/sql_tablesample.h)
  • The syntax for TABLESAMPLE clause is quite similar to the LIMIT clause. So, the implementation is inspired from it too. Like the LIMIT clause, support for variable percentage in stored procedure and query param has been implemented as well. (sql/sp_head.cc, sql/item.cc, sql/item.h, sql/sql_prepare.cc, sql/sql_lex.h, sql/sql_lex.cc, sql/sql_string.cc, sql/sql_string.h, sql/sql_type.h)
  • Extended grammar for table primary in SELECT clause with an optional TABLESAMPLE clause. (sql/sql_yacc.yy, sql/lex.h)
  • Extended definition of struct TABLE_LIST to accomodate Lex_tablesample. (sql/table.h)

Query Preparation (JOIN::prepare)

  • While the syntax analysis was handled in the previous section, the semantic analysis is done here. (sql/sql_base.cc)
    Things validated:
    • The sampled table is a user table and not a system table/view/derived table.
    • Sampling percentage is between 0 and 100. (sql/sql_tablesample.cc)
      This is done in Lex_tablesample::fix_tablesample_fields function. Some other things done in this function:
      • Initializes seed variables which are used in sampling rows.
      • Sets effective sampling method. For tables with no primary key, system sampling falls back to bernoulli sampling. For higher sampling percentages(set to >=30 for now), system sampling falls back to bernoulli sampling.
  • JOIN::prepare also has functions update_index_hint_maps and process_index_hints, which set keys_in_use_for_XXXX and other members according to the keys available and the specified index hints for this table. In presence of TABLESAMPLE clause, all indexes must be suppressed because their presence will cause the optimizer to choose a plan other than the one designated for TABLESAMPLE. So, all keys_in_use_for_XXXX and other members were cleared. (sql/table.cc, sql/opt_hints.cc)

Query Optimization

  • One of the parameters used by the optimizer to choose an optimal plan is the estimated number of rows that will be returned when the table is queried(without any conditions). This metric is stored in used_stat_records member of class TABLE. Scaled it down by the sampling percentage. (sql/sql_statistics.cc)
  • In estimate_scan_time function, some other statistics like records returned, read time etc are calculated. These are used in the best_access_path function to choose the best access path for fetching rows from the table. (sql/sql_select.cc)
    • In case of BERNOULLI sampling, the cost semantics will be similar to that of a full scan except they will be scaled down by a factor of sampling percentage.
    • In case of SYSTEM sampling, the cost semantics will be similar to that of an indexed row access.
  • In best_access_path, the access type is set either to JT_HASH or JT_SAMPLE for queries with TABLESAMPLE clause. (sql/sql_statistics.cc). In EXPLAIN for queries with TABLESAMPLE clause that also use hash join, hash_sample is shown as the access path. (sql/sql_select.h, sql/sql_select.cc)
  • In make_join_readinfo function, things are set up for the executor based on the access path chosen by the optimizer. For queries with TABLESAMPLE, it is set to join_init_read_record.

Query Execution

  • In join_init_read_record, init_read_record function is called. This sets the read_record_func function pointer of struct READ_RECORD to the appropriate function. For TABLESAMPLE clause, two new functions rr_sampling_bernoulli and rr_sampling_system were defined. (sql/records.cc)
    • rr_sampling_bernoulli uses the existing handler method ha_rnd_next to fetch all rows one by one. Then it generates a random number between 0 and 1, say x. If x's value is less than the sampling percentage p, then the row is added to the result row set.
    • rr_sampling_system uses a new handler ha_index_random_dive which returns a random row from the table(implementation details ahead). To avoid duplication, a hash set is used to save the keys of rows already in the result set.
  • ha_index_random_dive is a public api which handler class provides. Internally, it uses the virtual method index_random_dive which the storage engines must implement to support SYSTEM sampling in TABLESAMPLE clause. (sql/handler.h, sql/handler.cc)
  • For MyISAM, index_random_dive calls mi_random_dive. It works as follows:
    Let pos be the pointer to a page.
    1. Set pos to root of the index tree.
    2. Count all keys, say n.
    3. If internal node, there will be n + 1 child pointers. So, we choose a random number between 0 and n, say num. If leaf node, there will be n child pointers. So, we choose a random number between 0 and n - 1, say num.
    4. If internal node, we set pos equal to the child pointer at position num and repeat from step 2. If leaf node, we set pos equal to the child pointer at position num and break the loop.
    5. Read record that pos points to.
      (storage/myisam/ha_myisam.h, storage/myisam/ha_myisam.cc, storage/myisam/mi_random_dive.c)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements the SQL Standard TABLESAMPLE clause (supporting SYSTEM and BERNOULLI methods) for MariaDB. The changes introduce parser support, parameter handling for prepared statements and stored procedures, and mark these queries as unsafe for statement-based replication. The review feedback highlights several critical issues: a compilation or conversion bug in String::append_double due to an unsupported qs_append overload, safety and validation issues in fix_tablesample_fields (including unsafe reference pointers and incorrect handling of unbound parameters during PREPARE), silent conversion of NULL parameters to 0.0 in prepared statements, and misleading error reporting (ER_SYNTAX_ERROR instead of ER_NOT_SUPPORTED_YET) for semantic violations on views, derived tables, or system tables.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread sql/sql_string.cc
Comment on lines +588 to +596
bool String::append_double(double d)
{
if (realloc(str_length+FLOATING_POINT_BUFFER+2))
return TRUE;

qs_append(d);

return FALSE;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

Using qs_append(d) with a double argument is highly problematic. qs_append does not have an overload for double in String or Binary_string. This will result in either a compilation failure or an implicit conversion of the double to a char or integer type, leading to corrupted output in query logging and replication. Instead, use my_gcvt to format the floating-point number into a temporary buffer and then append it.

bool String::append_double(double d)
{
  char buf[FLOATING_POINT_BUFFER + 3];
  size_t len= my_gcvt(d, MY_GCVT_ARG_DOUBLE, sizeof(buf) - 1, buf, NULL);
  return append(buf, len);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is a qs_append with double argument at sql/sql_string.cc:802

Comment thread sql/sql_tablesample.h Outdated
Comment on lines +41 to +56
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

There are multiple issues in fix_tablesample_fields:

  1. Passing NULL as the reference pointer to fix_fields_if_needed is unsafe. If the item needs to be replaced during resolution, it will fail or cause a crash. Pass &sampling_percentage instead.
  2. The method does not check sampling_percentage->null_value. If a parameter is bound to NULL, it will be silently accepted as 0.0 instead of raising an error.
  3. For prepared statements, sampling_percentage->const_item() is true during PREPARE but the parameter has no value yet (state == Item_param::NO_VALUE). We must skip validation for unbound parameters during PREPARE to avoid incorrect failures.
  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, &sampling_percentage);
    if (err)
      DBUG_RETURN(1);
    if (sampling_percentage->const_item())
    {
      if (sampling_percentage->type() == Item::PARAM_ITEM)
      {
        Item_param *param= static_cast<Item_param*>(sampling_percentage);
        if (param->state == Item_param::NO_VALUE)
          DBUG_RETURN(0);
      }
      double d= sampling_percentage->val_real();
      if (sampling_percentage->null_value || d < 0.0 || d > 100.0)
        DBUG_RETURN(1);
    }
    DBUG_RETURN(0);
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  1. Fixed
  2. Fixed
  3. For prepared statements, sampling_percentage->const_item() is not true during PREPARE

Comment thread sql/sql_prepare.cc
Comment on lines +293 to +297
if (param->tablesample_clause_param && !param->has_double_value())
{
if (param->set_tablesample_clause_param(param->val_real()))
DBUG_RETURN(true);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If the tablesample parameter is NULL, calling set_tablesample_clause_param will convert the parameter's state to SHORT_DATA_VALUE with a value of 0.0. This silently converts NULL to 0.0 instead of preserving the NULL value to be caught as an error during execution. Check !param->is_null() before setting the parameter value.

      if (param->tablesample_clause_param && !param->has_double_value())
      {
        if (!param->is_null() && param->set_tablesample_clause_param(param->val_real()))
          DBUG_RETURN(true);
      }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Comment thread sql/sql_prepare.cc
Comment on lines +921 to +925
if (param->tablesample_clause_param && !param->has_double_value())
{
if (param->set_tablesample_clause_param(param->val_real()))
DBUG_RETURN(true);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If the tablesample parameter is NULL, calling set_tablesample_clause_param will convert the parameter's state to SHORT_DATA_VALUE with a value of 0.0. This silently converts NULL to 0.0 instead of preserving the NULL value to be caught as an error during execution. Check !param->is_null() before setting the parameter value.

        if (param->tablesample_clause_param && !param->has_double_value())
        {
          if (!param->is_null() && param->set_tablesample_clause_param(param->val_real()))
            DBUG_RETURN(true);
        }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Comment thread sql/sql_base.cc Outdated
Comment on lines +8571 to +8579
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);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using ER_SYNTAX_ERROR for semantic errors (such as attempting to use TABLESAMPLE on views, derived tables, or system tables, or when the sampling percentage is out of range) is misleading because the SQL syntax itself is correct. Instead, more specific errors should be reported. For unsupported table types, ER_NOT_SUPPORTED_YET is much more appropriate. Furthermore, if fix_tablesample_fields fails, it might have already set an error, so we should avoid blindly overwriting it with ER_SYNTAX_ERROR.

    if (table_list->tablesample_clause) {
      if (table_list->is_view_or_derived())
      {
        my_error(ER_NOT_SUPPORTED_YET, MYF(0), "TABLESAMPLE on views or derived tables");
        DBUG_RETURN(1);
      }
      if (table_list->table && table_list->table->s->table_category != TABLE_CATEGORY_USER)
      {
        my_error(ER_NOT_SUPPORTED_YET, MYF(0), "TABLESAMPLE on system tables");
        DBUG_RETURN(1);
      }
      if (table_list->tablesample_clause->fix_tablesample_fields(thd))
      {
        if (!thd->is_error())
          my_error(ER_WRONG_ARGUMENTS, MYF(0), "TABLESAMPLE");
        DBUG_RETURN(1);
      }
    }

@gkodinov gkodinov added the External Contribution All PRs from entities outside of MariaDB Foundation, Corporation, Codership agreements. label Jun 10, 2026
@vuvova vuvova added the GSoC label Jun 10, 2026
@vuvova
vuvova requested a review from Copilot June 10, 2026 15:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds initial parser + execution-time syntax validation support for the SQL:2016 TABLESAMPLE clause (SYSTEM/BERNOULLI) across regular statements, stored routines, and prepared statements, including basic binlog-unsafe marking and MTR coverage. Optimizer integration is explicitly out of scope for this stage.

Changes:

  • Extend the SQL grammar to parse TABLESAMPLE {SYSTEM|BERNOULLI} (<pct>) after table references and attach it to TABLE_LIST.
  • Add Lex_tablesample plus validation in setup_tables() to reject sampling on views/derived/CTE/system tables and to validate percentage range.
  • Add prepared-statement/SP plumbing for TABLESAMPLE parameters (binding + query rewrite logging), plus new MTR tests.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
sql/table.h Adds TABLE_LIST::tablesample_clause pointer to carry parsed TABLESAMPLE info.
sql/sql_yacc.yy Introduces TABLESAMPLE tokens and grammar rules; attaches Lex_tablesample to table references; marks statement binlog-unsafe for non-trivial sampling.
sql/sql_type.h Renames LIMIT-type predicate to a generalized numeric-clause predicate (now used by TABLESAMPLE variable handling).
sql/sql_tablesample.h New Lex_tablesample container and fix_tablesample_fields() validation helper.
sql/sql_string.h Adds String::append_double() declaration (used for rewritten query logging).
sql/sql_string.cc Implements String::append_double().
sql/sql_prepare.cc Adds TABLESAMPLE parameter binding/conversion to double for prepared statements (incl. logging path).
sql/sql_lex.h Adds BINLOG_STMT_UNSAFE_TABLESAMPLE and new LEX helpers to create TABLESAMPLE SP-variable items.
sql/sql_lex.cc Refactors LIMIT variable creation into a shared numeric helper and adds TABLESAMPLE SP-variable creators.
sql/sql_base.cc Validates TABLESAMPLE usage in setup_tables() (disallow view/derived/system, validate percentage).
sql/sp_head.cc Extends SP variable query rewrite logging to print TABLESAMPLE parameters as doubles.
sql/lex.h Adds TABLESAMPLE and BERNOULLI keywords.
sql/item.h Tracks TABLESAMPLE parameter-ness on rewritable items; adds double-typed binding helpers on Item_param; renames SP-variable numeric validation helper.
sql/item.cc Adds TABLESAMPLE handling in Item_param::set_from_item().
mysql-test/main/tablesample.test New MTR coverage for parsing/validation across basic queries, PS, SP, and disallowed table types.
mysql-test/main/tablesample.result Expected output for the new TABLESAMPLE MTR test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sql/item.h
Comment on lines +3402 to 3405
if (type_handler()->is_numeric_clause_valid_type())
return true;
my_error(ER_WRONG_SPVAR_TYPE_IN_LIMIT, MYF(0));
return false;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Comment thread sql/sql_type.h
Comment on lines +4107 to 4110
virtual bool is_numeric_clause_valid_type() const
{
return false;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@deo002
deo002 force-pushed the feat/tablesample branch from d9a5fec to 3b4cbab Compare June 10, 2026 19:00
@vuvova
vuvova requested a review from sanja-byelkin June 10, 2026 19:55
@deo002
deo002 force-pushed the feat/tablesample branch from 3b4cbab to a6c485f Compare June 11, 2026 19:06
@deo002
deo002 force-pushed the feat/tablesample branch from 4a2298d to fd0823b Compare July 9, 2026 16:46
@deo002
deo002 force-pushed the feat/tablesample branch from fd0823b to d510b4f Compare August 9, 2026 09:26
deo002 added 4 commits August 22, 2026 16:11
…lause

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.
Signed-off-by: deo002 <oberoidearsh@gmail.com>
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 <oberoidearsh@gmail.com>
@deo002
deo002 force-pushed the feat/tablesample branch 6 times, most recently from 0c019fa to cbbf154 Compare August 22, 2026 13:45
Signed-off-by: deo002 <oberoidearsh@gmail.com>
…_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 <oberoidearsh@gmail.com>
@deo002
deo002 force-pushed the feat/tablesample branch 2 times, most recently from 0f75ac3 to 1ab1d41 Compare August 27, 2026 18:20
@deo002
deo002 marked this pull request as ready for review August 28, 2026 03:26

@gkodinov gkodinov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for your contribution. This is a preliminary review: https://github.com/MariaDB/server/blob/main/COMMUNITY_CONTRIBUTIONS.md#preliminary-review.

I would consider squashing some of the commits, unless you need them separate for the review. If so, please state that.

Otherwise, LGTM

…LESAMPLE 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 <oberoidearsh@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

External Contribution All PRs from entities outside of MariaDB Foundation, Corporation, Codership agreements. GSoC

Development

Successfully merging this pull request may close these issues.

4 participants