【TcaplusDB知识库】条件过滤说明与更新

1. 介绍

针对更灵活的数据访问操作,TcaplusDB 支持条件操作的能力,具备以下能力:

  • 条件查询: 对单条记录或批量记录查询时,可指定记录级别的过滤条件,要求返回记录满足条件。
  • 条件更新: 对单条记录或批量记录修改或删除时,可指定记录级别的过滤条件,只有条件满足时才会执行变更。
  • 数组更新: 数组类型的字段(如 protobuf 中的 repeated 字段),支持对数组中元素进行增删改操作,可以只处理满足某些条件过滤的元素。
  • 数组查询: 数组类型的字段(如 protobuf 中的 repeated 字段),查询数组中返回满足过滤条件的或者某些下标范围内的数据,而不是完整的记录。

generic 表和 list 表都支持条件操作。

2. 示例表定义

这里分别定义两种类型的表,用于本章节示例,为了更直观说明条件过滤和更新。本章节以protobuf协议作为示例,但TDR协议同样支持条件过滤和更新,使用类似。

  • Generic 表
syntax = "proto3";
package myTcaplusTable;
import "tcaplusservice.optionv1.proto";

message user {
    option(tcaplusservice.tcaplus_primary_key) = "id,name";
    option(tcaplusservice.tcaplus_index) = "name_index(name)";

    message mail {
        string title = 1;
        string content = 2;
    }

    int32 id = 1;
    string name = 2;
    int32 rank = 3;
    repeated int64 gameids = 4;
    repeated mail mailbox = 5;
}
  • List 表
syntax = "proto3";
package myTcaplusTable;
import "tcaplusservice.optionv1.proto";

message list_user {
    option(tcaplusservice.tcaplus_primary_key) = "id,name";
    option(tcaplusservice.tcaplus_customattr) = "TableType=SORTLIST;ListNum=1024;SortField=rank";

    message mail {
        string title = 1;
        string content = 2;
    }

    int32 id = 1;
    string name = 2;
    int32 rank = 3;
    repeated int64 gameids = 4;
    repeated mail mailbox = 5;
}

3. 条件过滤说明

支持记录级别的条件过滤,只有满足条件,才对指定的(一个或多个)记录进行操作,包括对记录修改、删除或查询等。

3.1 解决什么问题

若没有条件过滤,对于 generic 表,通过主键查询或操作一个记录,若对应主键不存在,则返回错误码 TXHDB_ERR_RECORD_NOT_EXIST。

而条件则在这基础上再加一层过滤,对于 generic 表,主键 key 对应存在基础上,必须条件满足才能查询或操作对应的记录,否则返回错误码 COMMON_ERR_CONDITION_NOT_MATCHED。

key + condition,相当于 SQL 中的 where 语句,虽然能力比 SQL 还差不少,但已经提供一定程度的灵活能力。 特别是对于“读判断-然后写”的原子操作场景特别有用,条件更新的初衷就是解决这个问题的。

例如,对于 user 表,若 gameids 数组不包含 101 这个元素,那么在该数组插入 101,那么应用代码会写成如下

user u;
// 设置主键
u.set_id(1);
u.set_name("a");
int ret = api.Get(&u);
// ...
if (!Find(u.gameids(), 101)) // Find是应用实现的函数,在数组中找是否存在某元素
{
    u.add_gameids(101);
    ret = api.Set(&u);
    // ...
}

上述代码存在几个问题

  • 应用端和服务端存在多处交互,先 Get 再 Set。
  • 若 user 结构较大,那么这个流程中涉及的序列化、反序列带来不必要的开销较大。
  • 最严重的是,Get + Set 两次交互,这个逻辑不是原子的,若应用端存在多个这样流程并发,可能重复插入 101,这造成逻辑错误。由于 TcaplusDB 还不支持事务,应用解决该问题比较繁琐。

针对上述问题,条件更新可以解决一些交互多、非原子等问题,示例条件操作代码如下:

user u;
// 设置主键
u.set_id(1);
u.set_name("a");
// 先判断gameids是否已经包含101,若不存在(条件不满足)再插入101
int ret = api.UpdateItem(&u, "PUSH gameids#[-1][$ = 101]", "gameids NOT CONTAINS($==101)");
if (ret == COMMON_ERR_CONDITION_NOT_MATCHED) // 条件不满足,说明gameids已经存在101了
{
    // ...
}

上述代码,"gameids NOT CONTAINS($==101)"就是过滤条件,而"PUSH gameids#[-1][$ = 101]"是数组操作语句,后文介绍,这里指在 gameids 数组尾部插入 101。

3.2 条件过滤接口说明

TcaplusDB 提供的一些 protobuf API 新增了 const std::string &condition 入参,用于指定过滤条件,支持 condition 的接口以及新增的错误码详见后文的附录。

一些接口的使用示例如下,更多的示例可见详细 example。

// tcaplus_coroutine_pb_api.h
int Set(::google::protobuf::Message *msg, const std::string &operation = "", const std::string &condition = "");

int FieldInc(const std::set<std::string> &dottedpaths, ::google::protobuf::Message *msg, const std::string &operation = "", const std::string &condition = "");

int Traverse(::google::protobuf::Message *msg, const std::string &condition, TcaplusTraverseCallback *cb);

// example.cpp
user u;
// 设置主键
u.set_id(1);
u.set_name("a");
// 设置其他内容
// ...
// 当rank>100,才执行Set操作,若条件不满足会返回对应的错误码
int ret = api.Set(&u, "", "rank > 100");
if (ret == COMMON_ERR_CONDITION_NOT_MATCHED) { ... } // 条件不满足的情况

// 设置递增的步长
u.set_rank(1);
// 当rank达到上限100之后,不在对rank递增,否则 +1
std::set<std::string> dottedpaths;
dottedpaths.insert("rank");
ret = api.FieldInc(dottedpaths, &u, "", "rank < 100");

// 遍历2021-01-01年之后修改过的记录,这里$.LastAccessTime是记录的内建属性,表示记录的最后更新时间
int ret = api.Traverse(&u, "$.LastAccessTime >= \"2021-01-01\"", &traverseCallback);

3.3 条件过滤语法说明

过滤条件是类 SQL 的 where 语句的语法,已支持以下几种过滤能力

  • 比较,如 rank > 1 ,比较符有 >, >=, <, <=, =, ==, !=,在比较的上下文中, = 也是相等比较,而在其他语境下可能是赋值符号 。
  • 逻辑运算,如 rank > 1 AND rank < 10,运算符有 AND, OR, NOT
  • 位运算,仅支持“与”,如 filter & 8,当从低往高的第三位为 1 时,该表达式为 true。
  • CONTAINSNOT CONTAINS,即判断是否包含,如 "mailbox CONTAINS(title == \"tcaplus\")" 表示要求 mailbox 包含一个 title 等于"tcaplus"的元素。CONTAINS 括号中可以是更复杂的子条件。
  • 内建属性 $.LastAccessTime ,该内建属性表示记录的最后更新时间,最小精度为秒,可用于和字符串表示的时间进行比较,如"2021"、“2021-01-01"或"2021-01-01 00:00:00”。
  • 当前数组元素的引用$,如,"gameids NOT CONTAINS($==101)"

完整语法如下

condition_expr ::=
      array CONTAINS '(' condition ')'
    | array NOT CONTAINS '(' condition ')'
    | condition

condition ::=
      operand
    | operand comparator operand
    | condition AND condition
    | condition OR condition
    | NOT condition
    | operand bitwise_op operand

comparator ::=
      ==
    | =
    | <
    | >
    | <=
    | >=
    | !=

bitwise_op ::=
    &

array ::=
    identifier

operand ::=
      identifier
    | number
    | string
    | $
    | $.LastAccessTime
  • 语法说明
    • identifier: 一个合法的标识名称,在这里是字段名或字段名路径,如namemail.title
    • number: 整型或浮点数,不支持大整数。
    • string: 双引号或者单引号括起来的字符串。
  • 比较
    • 不同精度的整型或浮点型的数值都是可以相互比较的,这和 C++语言中是一致的,例如 int16 和 int32 比较,前者的类型会被提升之后再比较,整型和浮点比较,整型则会先被提升为浮点型。
    • int 和 uint 可比较,会先比较符号位。
    • 浮点型的等值会有精度偏差。
    • 字符串也是可比较,按照字母字典序,这和 C++中的 std::string 的比较行为是一致的。
    • 数值类型和字符串直接不可比较。
  • 操作符优先级
    • 条件表达式 condition 中,操作符优先级,从高到低为 comparator NOT AND OR ,例如 "a==1 OR a>10 AND a<20" ,会先计算 AND 的结果再计算 OR。
    • 当然可以使用括号来分隔条件表达式,例如 "(a==1 OR a>10) AND a<20" 则就先计算 OR。

3.4 性能优化建议

条件过滤的性能和 1)条件表达式、2)表的模式 有关,满足以下规则时,有针对性的性能优化(仅供参考,内部实现可能会调整):

  • 当条件表达式只用到 key 字段(包括主键和 index 字段)和记录的属性字段(如$.LastAccessTime),仅通过记录的主键或索引即可进行判断,无需从存储引擎读取全量数据。
  • 对于 SortList 表,当条件表达式只使用了 sort 字段,也有性能优化。
  • 对于 SortList 表,表定义的排序字段只有一个,且条件表达式是简单的二元比较(如"field >= 1""field == 1"等),有更好的性能优化,即使用二分查找。

img

TcaplusDB是腾讯出品的分布式NoSQL数据库,存储和调度的代码完全自研。具备缓存+落地融合架构、PB级存储、毫秒级时延、无损水平扩展和复杂数据结构等特性。同时具备丰富的生态、便捷的迁移、极低的运维成本和五个九高可用等特点。客户覆盖游戏、互联网、政务、金融、制造和物联网等领域。

猜你喜欢

转载自blog.csdn.net/weixin_44545651/article/details/121476059