Range 分区使用 timestamp 类型无法实现分区裁剪

首先,简单创建一个 timestamp 类型的 range 分区表

CREATE TABLE quarterly_report_status (
    report_id INT NOT NULL,
    report_status VARCHAR(20) NOT NULL,
    report_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) 
PARTITION BY RANGE (UNIX_TIMESTAMP(report_updated)) (
    PARTITION p0 VALUES LESS THAN (UNIX_TIMESTAMP('2008-01-01 00:00:00')),
    PARTITION p1 VALUES LESS THAN (UNIX_TIMESTAMP('2008-04-01 00:00:00')),
    PARTITION p2 VALUES LESS THAN (UNIX_TIMESTAMP('2010-01-01 00:00:00')),
    PARTITION p3 VALUES LESS THAN (MAXVALUE)
);

这时候执行

select * from quarterly_report_status where report_updated > UNIX_TIMESTAMP('2008-01-01 00:00:00');

或者

select * from quarterly_report_status where report_updated > '2008-01-01 00:00:00';

都会发现整个执行计划是进行了全表扫描,没有进行分区裁剪,只有在执行

select * from quarterly_report_status where report_updated = '2008-01-01 00:00:00';

可以触发分区裁剪。

同时,如果 report_updated 类型为 datetime 且使用 partition by range columns(report_updated) 作为分区,这时候查询区间范围内的数据又可以实现分区裁剪的功能。

这个地方的使用方面非常困惑,不确定是不是一个 bug。

1赞

请问: 集群是哪个版本? 目前 2.1 版本默认是关闭的,开启需要设置tidb_enable_table_partition为 1。此版本对分区的支持还不够完善。如果未开启分区功能,create table partition语句语法上也是支持的,但效果仍然是普通表。 3.0 版本默认支持分区功能。

v3.0.2 版本。

我验证过其他类型的 range 和 range columns 都是可以直接分区裁剪的,唯独这个 range timestamp 类型不行。看源码里面的 partition_pruning.result 的测试结果也是没办法裁剪的。

我觉得这是 bug,应该可以修复掉… 我试一下

如果方便的话,也告诉我下具体是哪块代码?我也研究下怎么修复

提了一个 PR 简单修复一下 unix_timestamp 的问题

1赞

然后解释一下能裁剪和不能裁剪的场景,以及为什么…

select * from quarterly_report_status where report_updated > UNIX_TIMESTAMP('2008-01-01 00:00:00');

这个不能裁剪,是因为 UNIX_TIMESTAMP(‘2008-01-01 00:00:00’) 的结果实际上是一个 int 类型,而 report_updated 是 timestamp 类型,在这两者比较的时候,cast 规则有一点问题,int 类型转 timestamp 类型后转成了 nil (bug)

你可以验证这一点:

mysql> explain select * from quarterly_report_status partition (p0) where report_updated > UNIX_TIMESTAMP('2008-01-01 00:00:00');
+---------------------+----------+------+------------------------------------------------------------------------------------------------+
| id                  | count    | task | operator info                                                                                  |
+---------------------+----------+------+------------------------------------------------------------------------------------------------+
| TableReader_8       | 0.00     | root | data:Selection_7                                                                               |
| └─Selection_7       | 0.00     | cop  | gt(test.quarterly_report_status.report_updated, NULL)                                          |
|   └─TableScan_6     | 10000.00 | cop  | table:quarterly_report_status, partition:p0, range:[-inf,+inf], keep order:false, stats:pseudo |
+---------------------+----------+------+------------------------------------------------------------------------------------------------+
3 rows in set, 1 warning (0.00 sec)

实际上使用的 条件是 report_updated > nil 恒为 true 的,所以分区裁剪模块裁剪不掉

gt(test.quarterly_report_status.report_updated, NULL)

我记录了另外一个 issue https://github.com/pingcap/tidb/issues/12028

然后

select * from quarterly_report_status where report_updated > '2008-01-01 00:00:00';

这个裁剪不掉,是因为你条件没写对,它本来就落在 p0 p1 p2 p3 分区里面,你要改成小于是能裁剪的,比如

select * from quarterly_report_status where report_updated < '2007-01-01 00:00:00';

再然后

select * from quarterly_report_status where report_updated > '2008-05-01 00:00:00';

这个的裁剪做的不准确,裁剪后应该是 p2 p3,而当前的 TiDB 裁剪后是 p0 p2 p3 原因是要做大于,小于,这类场景 p0 不太好裁剪,因为 “当计算结果为 NULL 的时候默认在 p0 分区”,大于小于不好预测结果是否为 NULL (有待优化)

然后

select * from quarterly_report_status where unix_timestamp(report_updated) > '2019-05-01 00:00:00';

这个裁剪不掉,有好几个问题。一个是 unix_timestamp 没有下推,其实这是一个不必要的限制,当时跟同事争执了好久。反正就是 TiDB 目前有这个约束在:如果不能下推的函数,相关的表达式就不能执行分区裁剪。

另外一个就是如果分区条件是 f(x) op const,然后查询条件是 g(x) op const,op 是大于小于之类的,这种规则我们没有支持。也就是条件里面有两个函数存在的时候。这种理论上是没法实现的,只有特定的场景 f 跟 g 是同一个函数,规则不太通用。

恩之前贴的查询 sql 应该是有编辑上的偏差。 其实想要表达的意思就是条件应该只命中 p1 p2 p3 但是实际结果是没有进行裁剪而已。 我的锅。

对于 where unix_timestamp(report_updated) 这样的查询我也觉得没必要限制吧,绝大多数应用场景都不会有这么诡异的写法才对。SQL 语句可读性反而显得有点差。