通过全局二级索引加速表格存储上的数据查询

本文涉及的产品
对象存储 OSS,20GB 3个月
对象存储 OSS,恶意文件检测 1000次 1年
对象存储 OSS,内容安全 1000次 1年
简介: 名词定义 索引表:对主表某些列数据的索引,只能读不能写。 预定义列:表格存储为Schema-free模型,原则上一行数据可以写入任意列,无需在schema中指定。但是也可以在建表时预先定义一些列,以及其类型。

名词定义

  1. 索引表:对主表某些列数据的索引,只能读不能写。
  2. 预定义列:表格存储为Schema-free模型,原则上一行数据可以写入任意列,无需在schema中指定。但是也可以在建表时预先定义一些列,以及其类型。
  3. 单列索引:只为某一个列建立索引。
  4. 组合索引:多个列组合排序,组合索引中包含组合索引列1,列2。
  5. 索引表属性列:被映射到索引表非PK列中的主表预定义列。
  6. 索引列补齐:自动将没有出现在索列中的主表PK列补充到索引表PK中。

使用场景介绍

全局二级索引是表格存储提供的一个新特性。当用户创建一张表时,其所有PK列构成了该表的“一级索引”:即给定完整PK,可以迅速的查找到该PK所在行的数据。但是越来越多的业务场景中,需要对表的属性列,或者非首列PK进行条件上的查询,由于没有足够的索引信息,只能通过进行全表的扫描,配合条件过滤,来得到最终结果,特别是全表数据较多,但最终结果很少时,全表扫描将浪费极大的资源。TableStore提供的二级索引功能(与DynamoDB及HBase的Phoenix方案类似),支持在指定列上建立索引,生成的索引表中数据按用户指定的索引列进行排序,主表的每一笔写入都将自动异步同步到索引表。用户只向主表中写入数据,根据索引表进行查询,在许多场景下,将极大的提高查询的效率。
以我们常见的电话话单查询为例,有主表如下:

CellNumber StartTime(Unix时间戳) CalledNumber Duration BaseStationNumber
123456 1532574644 654321 60 1
234567 1532574714 765432 10 1
234567 1532574734 123456 20 3
345678 1532574795 123456 5 2
345678 1532574861 123456 100 2
456789 1532584054 345678 200 3

其中CellNumberStartTime分别代表主叫号码通话发生时间,作为表的联合主键,CalleNumberDurationBaseStationNumber三列为表的预定义列,分别代表被叫号码通话时长基站号码,每次用户通话结束后,都会将此次通话的信息记录到该表中。可以分别在被叫号码基站号码列上建立二级索引,来满足不同角度的查询需求(具体建立索引的示例代码见本文附录)。

现在来看几种查询需求:
1.查询号码234567的所有主叫话单:由于表格存储为全局有序模型,所有行按主键进行排序,并且提供顺序扫描(getRange)接口,所以只需要在调用getRange接口时,将PK0列的最大及最小值均设置为234567,PK1列(通话发生时间)的最小值设置为0,最大值设置为INT_MAX,对主表进行扫描即可:

   private static void getRangeFromMainTable(SyncClient client, long cellNumber)
   {
       RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(TABLE_NAME);

       // 构造主键
       PrimaryKeyBuilder startPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
       startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.fromLong(cellNumber));
       startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.fromLong(0));
       rangeRowQueryCriteria.setInclusiveStartPrimaryKey(startPrimaryKeyBuilder.build());

       // 构造主键
       PrimaryKeyBuilder endPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
       endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.fromLong(cellNumber));
       endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.INF_MAX);
       rangeRowQueryCriteria.setExclusiveEndPrimaryKey(endPrimaryKeyBuilder.build());

       rangeRowQueryCriteria.setMaxVersions(1);

       String strNum = String.format("%d", cellNumber);
       System.out.println("号码" + strNum + "的所有主叫话单:");
       while (true) {
           GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
           for (Row row : getRangeResponse.getRows()) {
               System.out.println(row);
           }

           // 若nextStartPrimaryKey不为null, 则继续读取.
           if (getRangeResponse.getNextStartPrimaryKey() != null) {
               rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
           } else {
               break;
           }
       }
   }

2.查询号码123456的被叫话单:前面提到,表格存储的模型是对所有行按照主键进行排序,由于被叫号码存在于表的预定义列中,所以无法进行快速查询。因此可以在被叫号码索引表上进行查询:
索引表IndexOnBeCalledNumber

PK0 PK1 PK2
CalledNumber CellNumber StartTime
123456 234567 1532574734
123456 345678 1532574795
123456 345678 1532574861
654321 123456 1532574644
765432 234567 1532574714
345678 456789 1532584054

这里注意,系统会自动进行索引列补齐,即把主表的PK添加到索引列后面,共同作为索引表的PK。所以可以看到索引表有三列PK。

由于索引表IndexOnBeCalledNumber是按被叫号码作为主键,所以可以直接扫描索引表得到结果:

 private static void getRangeFromIndexTable(SyncClient client, long cellNumber) {
     RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX0_NAME);

     // 构造主键
     PrimaryKeyBuilder startPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
     startPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_1, PrimaryKeyValue.fromLong(cellNumber));
     startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MIN);
     startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.INF_MAX);
     rangeRowQueryCriteria.setInclusiveStartPrimaryKey(startPrimaryKeyBuilder.build());

     // 构造主键
     PrimaryKeyBuilder endPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
     endPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_1, PrimaryKeyValue.fromLong(cellNumber));
     endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MAX);
     endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.INF_MAX);
     rangeRowQueryCriteria.setExclusiveEndPrimaryKey(endPrimaryKeyBuilder.build());

     rangeRowQueryCriteria.setMaxVersions(1);

     String strNum = String.format("%d", cellNumber);
     System.out.println("号码" + strNum + "的所有被叫话单:");
     while (true) {
         GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
         for (Row row : getRangeResponse.getRows()) {
             System.out.println(row);
         }

         // 若nextStartPrimaryKey不为null, 则继续读取.
         if (getRangeResponse.getNextStartPrimaryKey() != null) {
             rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
         } else {
             break;
         }
     }
 }

3.查询基站002从时间1532574740开始的所有话单:与上述示例类似,但是查询不仅把基站号码列作为条件,同时把通话发生时间列作为查询条件,因此我们可以在基站号码通话发生时间列上建立组合索引,数据库中的记录将会如下所示:

索引表IndexOnBaseStation1:

PK0 PK1 PK2
BaseStationNumber StartTime CellNumber
001 1532574644 123456
001 1532574714 234567
002 1532574795 345678
002 1532574861 345678
003 1532574734 234567
003 1532584054 456789

然后在IndexOnBaseStation1索引表上进行查询:

 private static void getRangeFromIndexTable(SyncClient client,
                                            long baseStationNumber,
                                            long startTime) {
     RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX1_NAME);

     // 构造主键
     PrimaryKeyBuilder startPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
     startPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_3, PrimaryKeyValue.fromLong(baseStationNumber));
     startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.fromLong(startTime));
     startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MIN);
     rangeRowQueryCriteria.setInclusiveStartPrimaryKey(startPrimaryKeyBuilder.build());

     // 构造主键
     PrimaryKeyBuilder endPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
     endPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_3, PrimaryKeyValue.fromLong(baseStationNumber));
     endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.INF_MAX);
     endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MAX);
     rangeRowQueryCriteria.setExclusiveEndPrimaryKey(endPrimaryKeyBuilder.build());

     rangeRowQueryCriteria.setMaxVersions(1);

     String strBaseStationNum = String.format("%d", baseStationNumber);
     String strStartTime = String.format("%d", startTime);
     System.out.println("基站" + strBaseStationNum + "从时间" + strStartTime + "开始的所有被叫话单:");
     while (true) {
         GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
         for (Row row : getRangeResponse.getRows()) {
             System.out.println(row);
         }

         // 若nextStartPrimaryKey不为null, 则继续读取.
         if (getRangeResponse.getNextStartPrimaryKey() != null) {
             rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
         } else {
             break;
         }
     }
 }

4.查询发生在基站003上时间从15325748611532584054的所有通话记录的通话时长:此查询相比前面变的更为复杂,不仅把基站号码列与通话发生时间列作为了查询条件,而且只把通话时长列作为返回结果,可以仍使用3中的索引,查索引表成功后反查主表得到通话时长:

  private static void getRowFromIndexAndMainTable(SyncClient client,
                                                  long baseStationNumber,
                                                  long startTime,
                                                  long endTime,
                                                  String colName) {
      RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX1_NAME);

      // 构造主键
      PrimaryKeyBuilder startPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
      startPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_3, PrimaryKeyValue.fromLong(baseStationNumber));
      startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.fromLong(startTime));
      startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MIN);
      rangeRowQueryCriteria.setInclusiveStartPrimaryKey(startPrimaryKeyBuilder.build());

      // 构造主键
      PrimaryKeyBuilder endPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
      endPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_3, PrimaryKeyValue.fromLong(baseStationNumber));
      endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.fromLong(endTime));
      endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MAX);
      rangeRowQueryCriteria.setExclusiveEndPrimaryKey(endPrimaryKeyBuilder.build());

      rangeRowQueryCriteria.setMaxVersions(1);

      String strBaseStationNum = String.format("%d", baseStationNumber);
      String strStartTime = String.format("%d", startTime);
      String strEndTime = String.format("%d", endTime);

      System.out.println("基站" + strBaseStationNum + "从时间" + strStartTime + "到" + strEndTime + "的所有话单通话时长:");
      while (true) {
          GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
          for (Row row : getRangeResponse.getRows()) {
              PrimaryKey curIndexPrimaryKey = row.getPrimaryKey();
              PrimaryKeyColumn mainCalledNumber = curIndexPrimaryKey.getPrimaryKeyColumn(PRIMARY_KEY_NAME_1);
              PrimaryKeyColumn callStartTime = curIndexPrimaryKey.getPrimaryKeyColumn(PRIMARY_KEY_NAME_2);
              PrimaryKeyBuilder mainTablePKBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
              mainTablePKBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, mainCalledNumber.getValue());
              mainTablePKBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, callStartTime.getValue());
              PrimaryKey mainTablePK = mainTablePKBuilder.build(); // 构造主表PK

              // 反查主表
              SingleRowQueryCriteria criteria = new SingleRowQueryCriteria(TABLE_NAME, mainTablePK);
              criteria.addColumnsToGet(colName); // 读取主表的"通话时长"列
              // 设置读取最新版本
              criteria.setMaxVersions(1);
              GetRowResponse getRowResponse = client.getRow(new GetRowRequest(criteria));
              Row mainTableRow = getRowResponse.getRow();

              System.out.println(mainTableRow);
          }

          // 若nextStartPrimaryKey不为null, 则继续读取.
          if (getRangeResponse.getNextStartPrimaryKey() != null) {
              rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
          } else {
              break;
          }
      }
  }

为了提高查询效率,可以在基站号码列与通话发生时间列上建立组合索引,并把通话时长列作为索引表的属性列,数据库中的记录将会如下所示:

索引表IndexOnBaseStation2:

PK0 PK1 PK2 Defined0
BaseStationNumber StartTime CellNumber Duration
001 1532574644 123456 60
001 1532574714 234567 10
002 1532574795 345678 5
002 1532574861 345678 100
003 1532574734 234567 20
003 1532584054 456789 200

然后在IndexOnBaseStation2索引表上进行查询:

   private static void getRangeFromIndexTable(SyncClient client,
                                              long baseStationNumber,
                                              long startTime,
                                              long endTime,
                                              String colName) {
       RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX2_NAME);

       // 构造主键
       PrimaryKeyBuilder startPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
       startPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_3, PrimaryKeyValue.fromLong(baseStationNumber));
       startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.fromLong(startTime));
       startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MIN);
       rangeRowQueryCriteria.setInclusiveStartPrimaryKey(startPrimaryKeyBuilder.build());

       // 构造主键
       PrimaryKeyBuilder endPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
       endPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_3, PrimaryKeyValue.fromLong(baseStationNumber));
       endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.fromLong(endTime));
       endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MAX);
       rangeRowQueryCriteria.setExclusiveEndPrimaryKey(endPrimaryKeyBuilder.build());

       // 设置读取列
       rangeRowQueryCriteria.addColumnsToGet(colName);

       rangeRowQueryCriteria.setMaxVersions(1);

       String strBaseStationNum = String.format("%d", baseStationNumber);
       String strStartTime = String.format("%d", startTime);
       String strEndTime = String.format("%d", endTime);

       System.out.println("基站" + strBaseStationNum + "从时间" + strStartTime + "到" + strEndTime + "的所有话单通话时长:");
       while (true) {
           GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
           for (Row row : getRangeResponse.getRows()) {
               System.out.println(row);
           }

           // 若nextStartPrimaryKey不为null, 则继续读取.
           if (getRangeResponse.getNextStartPrimaryKey() != null) {
               rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
           } else {
               break;
           }
       }
   }

可见,如果不把通话时长列作为索引表的属性列,在每次查询时,都需先从索引表中解出主表的PK,然后对主表进行随机读。当然,把通话时长列作为索引表的属性列后,该列被同时存储在了主表及索引表中,增加了总的存储空间占用,这其实是性能与成本的一个平衡。

5.查询发生在基站003上时间从15325748611532584054的所有通话记录的总通话时长,平均通话时长,最大通话时长,最小通话时长:相对于上一条查询,这里不要求返回每一条通话记录的时长,只要求返回所有通话时长的统计信息,用户可以使用与上条查询同的的查询方式,然后自行对返回的每条通话时长做计算,得到最终结果,也可以使用SQL-on-OTS,省去客户端的计算,直接使用SQL语句返回最终统计结果,SQL-on-OTS的开通使用文档可见此处,其兼容绝大多数MySql语法,可以更方便的进行更复杂的,更贴近用户业务逻辑的计算。

功能介绍

  1. 支持全局二级索引,主表与索引表之间异步同步,正常情况下,同步延迟在毫秒级。
  2. 支持单列索引及组合索引,支持索引表带有属性列(Covered Indexes)。主表可以预先定义若干列(称为预定义列),可以对任意预定义列和主表PK列进行索引,可以指定主表的若干个预定义列作为索引表的属性列(索引表也可以不包含任何属性列)。当指定了主表的某些预定义列作为索引表的属性列时,读索引表可以直接得到主表中对应预定义列的值,无需反查主表,提高了查询性能(如上面示例4)。例如:主表有PK0, PK1, PK2三列主键,Defined0, Defined1, Defined2三列预定义列:

    • 索引列可以是PK2,没有属性列。
    • 索引列可以是PK2,属性列可以是Defined0。
    • 索引列可以是PK3, PK2,没有属性列。
    • 索引列可以是PK3, PK2,把Defined0作为属性列。
    • 索引列可以是PK2, PK1, PK0,把Defined0, Defined1, Defined2作为属性列。
    • 索引列可以是Defined0,没有属性列。
    • 索引列可以是Define0, PK1, 把Defined1作为属性列。
    • 索引列可以是Defined1, Defined0,没有属性列。
    • 索引列可以是Defined1, Defined0,把Defined2作为属性列。
  3. 支持稀疏索引(Sparse Indexes):即如果主表的某个预定义列作为索引表的属性列,当主表某行中不存在该预定义列时,只要索引列全部存在,仍会为此行建立索引,但如果部分索引列缺失,则不会为此行建立索引。例如:主表有PK0, PK1, PK2三列主键,Defined0, Defined1, Defined2三列预定义列,设置索引表PK为Defined0, Defined1, 索引表属性列为Defined2

    • 当主表某行中,只包含Defined0, Defined1这两列,不包含Defined2列时,会为此行建立索引。
    • 当主表某行中,只包含Defined0,Defined2这两列,不包含Defined1列时,不会为此行建立索引。
  4. 支持在已经存在的主表上进行创建,删除索引的操作。 后续版本将支持新建的索引表中包含主表中的存量数据。
  5. 查索引表不会自动反查主表,用户需要自行反查。后续版本将支持自动根据索引表反查主表的功能。

接口说明(以Java SDK为例)

  1. 创建主表时同时创建索引表

    private static void createTable(SyncClient client) {
        TableMeta tableMeta = new TableMeta(TABLE_NAME);
        tableMeta.addPrimaryKeyColumn(new PrimaryKeySchema(PRIMARY_KEY_NAME_1, PrimaryKeyType.STRING)); // 为主表设置PK列
        tableMeta.addPrimaryKeyColumn(new PrimaryKeySchema(PRIMARY_KEY_NAME_2, PrimaryKeyType.INTEGER)); // 为主表设置PK列
        tableMeta.addDefinedColumn(new DefinedColumnSchema(DEFINED_COL_NAME_1, DefinedColumnType.STRING)); // 为主表设置预定义列
        tableMeta.addDefinedColumn(new DefinedColumnSchema(DEFINED_COL_NAME_2, DefinedColumnType.INTEGER)); // 为主表设置预定义列
        tableMeta.addDefinedColumn(new DefinedColumnSchema(DEFINED_COL_NAME_3, DefinedColumnType.INTEGER)); // 为主表设置预定义列
    
        int timeToLive = -1; // 数据过期时间设置为永不过期
        int maxVersions = 1; // 最大版本数为1(带索引表的主表只支持版本数为1)
    
        TableOptions tableOptions = new TableOptions(timeToLive, maxVersions);
    
        ArrayList<IndexMeta> indexMetas = new ArrayList<IndexMeta>();
        IndexMeta indexMeta = new IndexMeta(INDEX_NAME); // 新建索引表Meta
        indexMeta.addPrimaryKeyColumn(DEFINED_COL_NAME_1); // 指定主表的DEFINED_COL_NAME_1列作为索引表的PK
        indexMeta.addDefinedColumn(DEFINED_COL_NAME_2); // 指定主表的DEFINED_COL_NAME_2列作为索引表的属性列
        indexMetas.add(indexMeta); // 添加索引表到主表
    
        CreateTableRequest request = new CreateTableRequest(tableMeta, tableOptions, indexMetas); // 创建主表
    
        client.createTable(request);
    }
  2. 为已经存在的主表添加索引表

    private static void createIndex(SyncClient client) {
        IndexMeta indexMeta = new IndexMeta(INDEX_NAME); // 新建索引Meta
        indexMeta.addPrimaryKeyColumn(DEFINED_COL_NAME_2); // 指定DEFINED_COL_NAME_2列为索引表的第一列PK
        indexMeta.addPrimaryKeyColumn(DEFINED_COL_NAME_1); // 指定DEFINED_COL_NAME_1列为索引表的第二列PK
        CreateIndexRequest request = new CreateIndexRequest(TABLE_NAME, indexMeta, false); // 将索引表添加到主表上
        client.createIndex(request); // 创建索引表
    }

    注意:当前在添加索引表时,尚不支持索引表中包含主表中已经存在的数据,即新建索引表中将只包含主表从创建索引表开始时的增量数据。如果有对存量数据建索引的需求,请钉钉联系表格存储技术支持

  3. 删除索引表

    private static void deleteIndex(SyncClient client) {
        DeleteIndexRequest request = new DeleteIndexRequest(TABLE_NAME, INDEX_NAME); // 指定主表名称及索引表名称
        client.deleteIndex(request); // 删除索引表
    }
  4. 读取索引表中数据

    需要返回的属性列在索引表中,直接读取索引表:

    private static void scanFromIndex(SyncClient client) {
        RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX_NAME); // 设置索引表名
    
        // 设置起始主键
        PrimaryKeyBuilder startPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
        startPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_1, PrimaryKeyValue.INF_MIN); // 设置需要读取的索引列最小值
        startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MIN); // 主表PK最小值
        startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.INF_MIN); // 主表PK最小值
        rangeRowQueryCriteria.setInclusiveStartPrimaryKey(startPrimaryKeyBuilder.build());
    
        // 设置结束主键
        PrimaryKeyBuilder endPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
        endPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_1, PrimaryKeyValue.INF_MAX); // 设置需要读取的索引列最大值
        endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MAX); // 主表PK最大值
        endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.INF_MAX); // 主表PK最大值
        rangeRowQueryCriteria.setExclusiveEndPrimaryKey(endPrimaryKeyBuilder.build());
    
        rangeRowQueryCriteria.setMaxVersions(1);
    
        System.out.println("扫描索引表的结果为:");
        while (true) {
            GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
            for (Row row : getRangeResponse.getRows()) {
                System.out.println(row);
            }
    
            // 若nextStartPrimaryKey不为null, 则继续读取.
            if (getRangeResponse.getNextStartPrimaryKey() != null) {
                rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
            } else {
                break;
            }
        }
    }

需要返回的属性列不在索引表中,需要反查主表:

   private static void scanFromIndex(SyncClient client) {
       RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX_NAME); // 设置索引表名

       // 设置起始主键
       PrimaryKeyBuilder startPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
       startPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_1, PrimaryKeyValue.INF_MIN); // 设置需要读取的索引列最小值
       startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MIN); // 主表PK最小值
       startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.INF_MIN); // 主表PK最小值
       rangeRowQueryCriteria.setInclusiveStartPrimaryKey(startPrimaryKeyBuilder.build());

       // 设置结束主键
       PrimaryKeyBuilder endPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
       endPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_1, PrimaryKeyValue.INF_MAX); // 设置需要读取的索引列最大值
       endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MAX); // 主表PK最大值
       endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.INF_MAX); // 主表PK最大值
       rangeRowQueryCriteria.setExclusiveEndPrimaryKey(endPrimaryKeyBuilder.build());

       rangeRowQueryCriteria.setMaxVersions(1);

       while (true) {
           GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
           for (Row row : getRangeResponse.getRows()) {
               PrimaryKey curIndexPrimaryKey = row.getPrimaryKey();
               PrimaryKeyColumn pk1 = curIndexPrimaryKey.getPrimaryKeyColumn(PRIMARY_KEY_NAME1);
               PrimaryKeyColumn pk2 = curIndexPrimaryKey.getPrimaryKeyColumn(PRIMARY_KEY_NAME2);
               PrimaryKeyBuilder mainTablePKBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
               mainTablePKBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME1, pk1.getValue());
               mainTablePKBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME2, ke2.getValue());
               PrimaryKey mainTablePK = mainTablePKBuilder.build(); // 根据索引表PK构造主表PK

               // 反查主表
               SingleRowQueryCriteria criteria = new SingleRowQueryCriteria(TABLE_NAME, mainTablePK);
               criteria.addColumnsToGet(DEFINED_COL_NAME3); // 读取主表的DEFINED_COL_NAME3列
               // 设置读取最新版本
               criteria.setMaxVersions(1);
               GetRowResponse getRowResponse = client.getRow(new GetRowRequest(criteria));
               Row mainTableRow = getRowResponse.getRow();
               System.out.println(row); 
           }

           // 若nextStartPrimaryKey不为null, 则继续读取.
           if (getRangeResponse.getNextStartPrimaryKey() != null) {
               rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
           } else {
               break;
           }
       }
   } 

注意事项

  1. 对于每张索引表,系统会自动进行索引列补齐。在对索引表进行扫描时,用户需要注意填充对应PK列的范围(一般为负无穷到正无穷)。例如:主表有PK0PK1两列PK,Defined0一列预定义列,如果用户指定在Defined0列上建立索引,则系统会将索引表的PK生成为Defined0, PK0, PK1。用户可以指定在Defined0列及PK1列上建立索引,则生成索引表的PK为Defined0,PK1,PK0。用户还可以在PK列上建立索引,则生成索引表的PK为PK1,PK0。但是无论如果,用户在建立索引表时,只需要指定自己关心的索引列,其它列会由系统自动添加。例如主表有PK0, PK1两列PK,Defined0作为预定义列:

    • 如果在Defined0上建立索引,那么生成的索引表PK将会是Defined0, PK0, PK1三列。
    • 如果在PK1上建立索引,那么生成的索引表PK将会是PK1, PK0两列。
  2. 选择主表的哪些预定义列作为主表的属性列,是成本和性能的平衡:将主表的一列预定义列作为索引表的属性列后,查询时不用反查主表即可得到该列的值,但是同时增加了相应的存储成本。反之则需要根据索引表反查主表。用户可以根据自己的查询模式以及成本的考虑,作出相应的选择。
  3. 不建议把时间相关列作为索引表PK的第一列,这样可能导致索引表更新速度变慢。建议将时间列进行哈希,然后在哈希后的列上建立索引,如果有类似需求可以钉钉联系表格存储技术支持一同讨论表结构。
  4. 不建议把区取值范围非常小,甚至可枚举的列作为索引表PK的第一列,例如性别,这样将导致索引表水平扩展能力受限,从而影响索引表写入性能。

限制项

  1. 同一张主表下,最多建立16张索引表。
  2. 对于一张索引表,其索引列最多有四列(为主表PK以及主表预定义列的任意组合)。
  3. 索引列的类型为整型,字符串,二进制,布尔, 与主表PK列的约束相同。
  4. 类型为字符串以及二进制的索引列,大小限制与主表PK列相同。
  5. 类型为字符串以及二进制的列,作为索引表的属性列时,限制与主表相同。
  6. 暂不支持TTL表建立索引,有需求请钉钉联系表格存储技术支持。
  7. 不支持使用多版本功能的表上建立索引。
  8. 索引表上不允许使用Stream功能。

附录

创建主表及索引表

    private static final String TABLE_NAME = "CallRecordTable";
    private static final String INDEX0_NAME = "IndexOnBeCalledNumber";
    private static final String INDEX1_NAME = "IndexOnBaseStation1";
    private static final String INDEX2_NAME = "IndexOnBaseStation2";
    private static final String PRIMARY_KEY_NAME_1 = "CellNumber";
    private static final String PRIMARY_KEY_NAME_2 = "StartTime";
    private static final String DEFINED_COL_NAME_1 = "CalledNumber";
    private static final String DEFINED_COL_NAME_2 = "Duration";
    private static final String DEFINED_COL_NAME_3 = "BaseStationNumber";
    
    private static void createTable(SyncClient client) {
        TableMeta tableMeta = new TableMeta(TABLE_NAME);
        tableMeta.addPrimaryKeyColumn(new PrimaryKeySchema(PRIMARY_KEY_NAME_1, PrimaryKeyType.INTEGER));
        tableMeta.addPrimaryKeyColumn(new PrimaryKeySchema(PRIMARY_KEY_NAME_2, PrimaryKeyType.INTEGER));
        tableMeta.addDefinedColumn(new DefinedColumnSchema(DEFINED_COL_NAME_1, DefinedColumnType.INTEGER));
        tableMeta.addDefinedColumn(new DefinedColumnSchema(DEFINED_COL_NAME_2, DefinedColumnType.INTEGER));
        tableMeta.addDefinedColumn(new DefinedColumnSchema(DEFINED_COL_NAME_3, DefinedColumnType.INTEGER));
    
        int timeToLive = -1; // 数据的过期时间, 单位秒, -1代表永不过期. 带索引表的主表数据过期时间必须为-1
        int maxVersions = 1; // 保存的最大版本数, 带索引表的请表最大版本数必须为1
    
        TableOptions tableOptions = new TableOptions(timeToLive, maxVersions);
    
        ArrayList<IndexMeta> indexMetas = new ArrayList<IndexMeta>();
        IndexMeta indexMeta0 = new IndexMeta(INDEX0_NAME);
        indexMeta0.addPrimaryKeyColumn(DEFINED_COL_NAME_1);
        indexMetas.add(indexMeta0);
        IndexMeta indexMeta1 = new IndexMeta(INDEX1_NAME);
        indexMeta1.addPrimaryKeyColumn(DEFINED_COL_NAME_3);
        indexMeta1.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2);
        indexMetas.add(indexMeta1);
        IndexMeta indexMeta2 = new IndexMeta(INDEX2_NAME);
        indexMeta2.addPrimaryKeyColumn(DEFINED_COL_NAME_3);
        indexMeta2.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2);
        indexMeta2.addDefinedColumn(DEFINED_COL_NAME_2);
        indexMetas.add(indexMeta2);
    
        CreateTableRequest request = new CreateTableRequest(tableMeta, tableOptions, indexMetas);
    
        client.createTable(request);
    }

Reference

  1. DynamoDB GSI
  2. HBase Phoenix

试用

目前表格存储的全局二级索引功能已经在张北公共云集群预发邀测,想要试用可以直接钉钉联系 表格存储技术支持 或者加入钉钉群 111789671

相关实践学习
阿里云表格存储使用教程
表格存储(Table Store)是构建在阿里云飞天分布式系统之上的分布式NoSQL数据存储服务,根据99.99%的高可用以及11个9的数据可靠性的标准设计。表格存储通过数据分片和负载均衡技术,实现数据规模与访问并发上的无缝扩展,提供海量结构化数据的存储和实时访问。 产品详情:https://www.aliyun.com/product/ots
目录
相关文章
|
6月前
|
存储 索引
表格存储根据多元索引查询条件直接更新数据
表格存储是否可以根据多元索引查询条件直接更新数据?
60 3
|
6月前
|
存储 Cloud Native 索引
hologres加速查询
hologres加速查询
|
10月前
|
NoSQL 开发工具
TableStore表格存储(阿里云OTS)多行数据操作查询,支持倒序,过滤条件和分页
1. 批量读取操作 批量读取操作可以通过多种方式进行,包括: GetRow:根据主键读取一行数据。 BatchGetRow:批量读取多行数据。 GetRange:根据范围读取多行数据。
560 0
|
SQL 存储 缓存
PolarDB-X 全局二级索引
索引是数据库的基础组件,早在1970年代,SystemR 就已经通过增加索引来支持多维度查询。单机数据库中,索引主要按照用途和使用的数据结构分为 BTree 索引、Hash 索引、全文索引、空间索引等。通常,每张表中包含一个主键索引(Primary Index),主键索引以外的索引,统称为二级索引(Secondary Index)。
PolarDB-X 全局二级索引
|
存储 SQL 缓存
表格存储Tablestore-多元索引最佳实践
本文将大量用户和业务在使用表格存储(Tablestore)的多元索引时候遇到的问题总结为最佳实践提供给大家。
778 0
|
SQL 存储 自然语言处理
表格存储最佳实践:使用多元索引加速 SQL 查询
表格存储(Tablestore)在 2022 年 5 月正式发布了 SQL 商业化版本,业务上只需要在数据表上建立映射关系,就可以基于 SQL 引擎方便地对表格存储中的数据进行访问和计算,大大地降低了用户的学习成本。
667 0
|
SQL 算法 关系型数据库
PolarDB-X 1.0-用户指南-全局二级索引-使用全局二级索引
PolarDB-X支持全局二级索引,本文将介绍如何创建、使用全局二级索引功能。
290 0
PolarDB-X 1.0-用户指南-全局二级索引-使用全局二级索引
|
SQL 关系型数据库 MySQL
PolarDB-X 1.0-用户指南-全局二级索引-使用全局二级索引时的注意事项
本文汇总了在使用全局二级索引时的相关注意事项。
171 0
|
关系型数据库 MySQL 分布式数据库
PolarDB-X 1.0-基本原理-全局二级索引
本文将介绍全局二级索引(Global Secondary Index,GSI)主要功能及常见问题。
314 0
PolarDB-X 1.0-基本原理-全局二级索引
|
SQL 存储 Java
表格存储 SQL 查询多元索引
多元索引是表格存储产品中一个重要的功能,多元索引使用倒排索引技术为表格存储提供了非主键列上的快速检索功能,另外也提供了统计聚合功能。表格存储近期开放了SQL查询功能,SQL引擎默认从原始表格中读取数据,非主键列上的查询需要扫描全表。
表格存储 SQL 查询多元索引