All Products
Search
Document Center

Tablestore:Scenarios

Last Updated:Sep 09, 2026

Secondary indexes improve query efficiency in various scenarios. You can create an index on specified columns. The data in the resulting index table is sorted by these index columns. When you write data to the primary table, the data is automatically synchronized to the index table, which you can then query to retrieve data quickly.

Example scenario

In a phone call record query scenario, information about each call is stored in a data table after the call ends.

The primary key and predefined columns of the data table are as follows:

  • CellNumber and StartTime are the primary keys. They represent the calling number and the call start time, respectively.

  • CalledNumber, Duration, and BaseStationNumber are predefined columns. They represent the called number, call duration, and base station number, respectively.

The following table shows sample data from the data table. The wide table model of Tablestore sorts all rows by primary key and provides the sequential scan (GetRange) operation to read data.

CellNumber

StartTime (UNIX timestamp)

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

Consider the following query requirements. You can retrieve the required data from the primary table, a global secondary index, or a local secondary index as needed.

  • Query all outgoing call records for the number 234567.

  • Query all incoming call records for the number 123456.

  • Query all call records from base station 002 that started at or after timestamp 1532574740.

  • Query the duration of all calls that occurred at base station 003 between timestamps 1532574861 and 1532584054.

  • Query the total, average, maximum, and minimum duration of all calls that occurred at base station 003 between timestamps 1532574861 and 1532584054.

  • Query all call records from the calling number 456789 to the called number 345678.

Data query

Select the appropriate query method for each requirement.

Implementation methods

The following table describes the implementation method for each query requirement.

Important
  • For an example of how to create a table and a secondary index, see Appendix: Example of creating a data table and secondary indexes.

    After you create the primary table, write the sample data to it. The data is automatically synchronized to the index table. For more information about how to write data, see Write data.

  • The system automatically performs primary key completion. This means that the primary key columns of the primary table are added to the index columns to form the primary key of the index table.

  • The first primary key column of a local secondary index must be the same as the first primary key column of the primary table.

Query requirement

Implementation method

Query all outgoing call records for the number 234567.

You can call the getRange interface to scan the data table.

Query all incoming call records for the number 123456.

Create an index on the CalledNumber column. Then, call the getRange operation to scan the index table.

Query all call records from base station 002 that started at or after timestamp 1532574740.

Create a compound index on the BaseStationNumber and StartTime columns. Then, call the getRange operation to scan the index table (a global secondary index).

Query the duration of all calls that occurred at base station 003 between timestamps 1532574861 and 1532584054.

Create a compound index on the BaseStationNumber and StartTime columns and return only the Duration column. Then, call the getRange operation to scan the index table (a global secondary index).

You can manually look up the primary table to retrieve the value of the Duration column or include the Duration column as an attribute column in the index table.

To query the total, average, maximum, and minimum duration of all calls that occurred at base station 003 between timestamps 1532574861 and 1532584054, you can use the same implementation method. Then, you can calculate the final results from the returned duration of each call.

Note

You can also use Tablestore SQL queries. This way, you do not need to perform calculations on the client. You can use an SQL statement to directly retrieve the final statistical results. For more information, see Query data.

Query all call records from the calling number 456789 to the called number 345678.

Create a compound index on the CellNumber and CalledNumber columns. Include the Duration and BaseStationNumber columns as attribute columns in the index table. Then, call the getRange operation to scan the index table (a local secondary index).

Query all outgoing call records for the number 234567

The CellNumber column is a primary key of the primary table, which supports fast queries. You can call the GetRange operation to scan the primary table and retrieve the results.

When you call the GetRange operation, set both the minimum and maximum values of the CellNumber column to 234567. Set the minimum value of the StartTime column to 0 and the maximum value to INT_MAX.

The following code provides an example:

// The cellNumber parameter in the example corresponds to the CellNumber column in the primary table.
private static void getRangeFromMainTable(SyncClient client, long cellNumber){
    RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(TABLE_NAME);

    // Construct the start primary key.
    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());

    // Construct the end primary key.
    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("All outgoing call records for number " + strNum + ":");
    while (true) {
        GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
        for (Row row : getRangeResponse.getRows()) {
            System.out.println(row);
        }

        // If nextStartPrimaryKey is not null, continue to read data.
        if (getRangeResponse.getNextStartPrimaryKey() != null) {
            rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
        } else {
            break;
        }
    }
}

Query all incoming call records for the number 123456

The query uses the CalledNumber column as a condition. However, CalledNumber is a predefined column in the primary table and does not support fast queries. To solve this issue, you can create an index named IndexOnBeCalledNumber on the CalledNumber column. Use CalledNumber as the primary key of the index table. Then, you can call the getRange operation to scan the index table and retrieve the results.

Because the first primary key column of the index table is different from the first primary key column of the primary table, the index is a global secondary index.

Index table data

The following table shows the data in the IndexOnBeCalledNumber index table.

PK0

PK1

PK2

CalledNumber

CellNumber

StartTime

123456

234567

1532574734

123456

345678

1532574795

123456

345678

1532574861

345678

456789

1532584054

654321

123456

1532574644

765432

234567

1532574714

When you call the GetRange operation, set both the minimum and maximum values of the CalledNumber column to 123456. Set the minimum values for the CellNumber and StartTime columns to INT_MIN and their maximum values to INT_MAX. The following code provides an example:

// The calledNumber parameter in the example corresponds to the CalledNumber column in the index table.
private static void getRangeFromIndexTable(SyncClient client, long calledNumber) {
    RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX0_NAME);

    // Construct the start primary key.
    PrimaryKeyBuilder startPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
    startPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_1, PrimaryKeyValue.fromLong(calledNumber));
    startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.INF_MIN);
    startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.INF_MIN);
    rangeRowQueryCriteria.setInclusiveStartPrimaryKey(startPrimaryKeyBuilder.build());

    // Construct the end primary key.
    PrimaryKeyBuilder endPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
    endPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_1, PrimaryKeyValue.fromLong(calledNumber));
    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", calledNumber);
    System.out.println("All incoming call records for number " + strNum + ":");
    while (true) {
        GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
        for (Row row : getRangeResponse.getRows()) {
            System.out.println(row);
        }

        // If nextStartPrimaryKey is not null, continue to read data.
        if (getRangeResponse.getNextStartPrimaryKey() != null) {
            rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
        } else {
            break;
        }
    }
}

Query all call records from base station 002 that started at or after timestamp 1532574740

The query uses the BaseStationNumber and StartTime columns as conditions. BaseStationNumber is a predefined column in the primary table and does not support fast queries. To solve this issue, you can create a compound index named IndexOnBaseStation1 on the BaseStationNumber and StartTime columns. Use these columns as the primary key of the index table. Then, you can call the getRange operation to scan the index table and retrieve the results.

Because the first primary key column of the index table is different from the first primary key column of the primary table, the index is a global secondary index.

Index table data

The following table shows the data in the IndexOnBaseStation1 index table.

PK0

PK1

PK2

BaseStationNumber

StartTime

CellNumber

1

1532574644

123456

1

1532574714

234567

2

1532574795

345678

2

1532574861

345678

3

1532574734

234567

3

1532584054

456789

When you call the GetRange API to query data, you can set the BaseStationNumber column with minimum and maximum values of 2, the StartTime column with a minimum value of 1532574740 and a maximum value of INT_MAX, and the CellNumber column with a minimum value of INT_MIN and a maximum value of INT_MAX. The following code is an example:

// The baseStationNumber parameter in the example corresponds to the BaseStationNumber column in the index table.
// The startTime parameter in the example corresponds to the start time for the StartTime column in the index table.
private static void getRangeFromIndexTable(SyncClient client,
                                           long baseStationNumber,
                                           long startTime) {
    RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX1_NAME);

    // Construct the start primary key.
    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());

    // Construct the end primary key.
    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("All incoming call records from base station " + strBaseStationNum + " starting from time " + strStartTime + ":");
    while (true) {
        GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
        for (Row row : getRangeResponse.getRows()) {
            System.out.println(row);
        }

        // If nextStartPrimaryKey is not null, continue to read data.
        if (getRangeResponse.getNextStartPrimaryKey() != null) {
            rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
        } else {
            break;
        }
    }
}

Query the duration of all calls that occurred at base station 003 between timestamps 1532574861 and 1532584054

Because you use the BaseStationNumber and StartTime columns as query conditions and retrieve only the Duration column, you can use the IndexOnBaseStation1 index table from the Query all call records for base station 002 starting from 1532574740 scenario to query the index table. You can then perform a reverse lookup on the data table to retrieve the call duration.

Because the first primary key column of the index table is different from the first primary key column of the primary table, the index is a global secondary index.

The following code provides an example:

// The baseStationNumber parameter in the example corresponds to the BaseStationNumber column in the index table.
// The startTime and endTime parameters in the example correspond to the start and end times for the StartTime column values to query in the index table.
// The DEFINED_COL_NAME_2 parameter in the example corresponds to the Duration column in the primary table to look up.
private static void getRowFromIndexAndMainTable(SyncClient client,
 long baseStationNumber,
 long startTime,
 long endTime) {
 RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX1_NAME);

 // Construct the start primary key.
 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());

 // Construct the end primary key.
 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("Call duration for all call records from base station " + strBaseStationNum + " from time " + strStartTime + " to " + strEndTime + ":");
 while (true) {
 GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
 for (Row row : getRangeResponse.getRows()) {
 PrimaryKey curIndexPrimaryKey = row.getPrimaryKey();
 // Construct the primary table primary key.
 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(); 

 // Look up the primary table.
 SingleRowQueryCriteria criteria = new SingleRowQueryCriteria(TABLE_NAME, mainTablePK);
 // Read the Duration column of the primary table.
 criteria.addColumnsToGet(DEFINED_COL_NAME_2); 
 // Set to read the latest version.
 criteria.setMaxVersions(1);
 GetRowResponse getRowResponse = client.getRow(new GetRowRequest(criteria));
 Row mainTableRow = getRowResponse.getRow();

 System.out.println(mainTableRow);
 }

 // If nextStartPrimaryKey is not null, continue to read data.
 if (getRangeResponse.getNextStartPrimaryKey() != null) {
 rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
 } else {
 break;
 }
 }
}

To improve query efficiency, you can create a compound index named IndexOnBaseStation2 on the BaseStationNumber and StartTime columns. Include the Duration column as an attribute column in the index table. Then, you can call the getRange operation to scan the index table and retrieve the results directly.

Index table data

The following table shows the data in the IndexOnBaseStation2 index table.

PK0

PK1

PK2

Defined0

BaseStationNumber

StartTime

CellNumber

Duration

1

1532574644

123456

60

1

1532574714

234567

10

2

1532574795

345678

5

2

1532574861

345678

100

3

1532574734

234567

20

3

1532584054

456789

200

When you call the GetRange operation, set both the minimum and maximum values of the BaseStationNumber column to "003". Set the minimum value of the StartTime column to 1532574861 and its maximum value to 1532584054. Set the minimum value of the CellNumber column to INT_MIN and its maximum value to INT_MAX.

The following code provides an example:

// The baseStationNumber parameter in the example corresponds to the BaseStationNumber column in the index table.
// The startTime and endTime parameters in the example correspond to the start and end times for the StartTime column values to query in the index table.
// The DEFINED_COL_NAME_2 parameter in the example corresponds to the Duration column in the index table.
private static void getRangeFromIndexTable(SyncClient client,
 long baseStationNumber,
 long startTime,
 long endTime) {
 RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX2_NAME);

 // Construct the start primary key.
 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());

 // Construct the end primary key.
 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());

 // Set the columns to read.
 rangeRowQueryCriteria.addColumnsToGet(DEFINED_COL_NAME_2);

 rangeRowQueryCriteria.setMaxVersions(1);

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

 System.out.println("Call duration for all call records from base station " + strBaseStationNum + " from time " + strStartTime + " to " + strEndTime + ":");
 while (true) {
 GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
 for (Row row : getRangeResponse.getRows()) {
 System.out.println(row);
 }

 // If nextStartPrimaryKey is not null, continue to read data.
 if (getRangeResponse.getNextStartPrimaryKey() != null) {
 rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
 } else {
 break;
 }
 }
}

Query all call records from the calling number 456789 to the called number 345678

The query uses the CellNumber and CalledNumber columns as conditions. CalledNumber is a predefined column in the primary table and does not support fast queries. To solve this issue, you can create a compound index named LocalIndexOnBeCalledNumber on the CellNumber and CalledNumber columns. Use these columns as the primary key of the index table. Include the Duration and BaseStationNumber columns as attribute columns. Then, you can call the getRange operation to scan the index table and retrieve the results.

Because the first primary key column of the index table is the same as the first primary key column of the primary table, the index is a local secondary index.

Index table data

The following table shows the data in the LocalIndexOnBeCalledNumber index table.

PK0

Defined0

PK1

Defined1

Defined2

CellNumber

CalledNumber

StartTime (UNIX timestamp)

Duration

BaseStationNumber

123456

654321

1532574644

60

1

234567

123456

1532574734

20

3

234567

765432

1532574714

10

1

345678

123456

1532574795

5

2

345678

123456

1532574861

100

2

456789

345678

1532584054

200

3

When you call the GetRange operation, set both the minimum and maximum values of the CellNumber column to 456789 and the CalledNumber column to 345678. Set the minimum value of the StartTime column to 0 and its maximum value to INT_MAX. The following code provides an example:

// The cellNumber and calledNumber parameters in the example correspond to the CellNumber and CalledNumber columns in the index table.
private static void getRangeFromLocalIndex(SyncClient client, long cellNumber, long calledNumber){

    RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(INDEX3_NAME);

    // Construct the start primary key.
    PrimaryKeyBuilder startPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
    startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.fromLong(cellNumber));
    startPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_1, PrimaryKeyValue.fromLong(calledNumber));
    startPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.fromLong(0));
    rangeRowQueryCriteria.setInclusiveStartPrimaryKey(startPrimaryKeyBuilder.build());

    // Construct the end primary key.
    PrimaryKeyBuilder endPrimaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
    endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1, PrimaryKeyValue.fromLong(cellNumber));
    endPrimaryKeyBuilder.addPrimaryKeyColumn(DEFINED_COL_NAME_1, PrimaryKeyValue.fromLong(calledNumber));
    endPrimaryKeyBuilder.addPrimaryKeyColumn(PRIMARY_KEY_NAME_2, PrimaryKeyValue.INF_MAX);
    rangeRowQueryCriteria.setExclusiveEndPrimaryKey(endPrimaryKeyBuilder.build());

    rangeRowQueryCriteria.setMaxVersions(1);

    String strNum = String.format("%d", cellNumber);
    String strCalledNum = String.format("%d", calledNumber);
    System.out.println("All call records between number " + strNum + " and number " +strCalledNum+ ":");
    while (true) {
        GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
        for (Row row : getRangeResponse.getRows()) {
            System.out.println(row);
        }

        // If nextStartPrimaryKey is not null, continue to read data.
        if (getRangeResponse.getNextStartPrimaryKey() != null) {
            rangeRowQueryCriteria.setInclusiveStartPrimaryKey(getRangeResponse.getNextStartPrimaryKey());
        } else {
            break;
        }
    }
}

Appendix: Example of creating a data table and secondary indexes

The following example shows how to create the primary table and secondary indexes, including local and global secondary indexes, that are used in this topic.

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 INDEX3_NAME = "LocalIndexOnBeCalledNumber";
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));
    // The time-to-live (TTL) of the data, in seconds. A value of -1 means that the data never expires. The TTL for a primary table with an index table must be -1.
    int timeToLive = -1; 
    // The maximum number of versions to save. The max versions for a primary table with an index table must be 1.
    int maxVersions = 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);
 
    IndexMeta indexMeta3 = new IndexMeta(INDEX3_NAME);
    indexMeta3.addPrimaryKeyColumn(PRIMARY_KEY_NAME_1);
    indexMeta3.addPrimaryKeyColumn(DEFINED_COL_NAME_1);
    indexMeta3.addDefinedColumn(DEFINED_COL_NAME_2);
    indexMeta3.addDefinedColumn(DEFINED_COL_NAME_3);
    // Set the index synchronization mode.
    indexMeta3.setIndexUpdateMode(IUM_SYNC_INDEX);
    // Set the index type to IT_LOCAL_INDEX (local secondary index).
    indexMeta3.setIndexType(IT_LOCAL_INDEX);
    indexMetas.add(indexMeta3);
 
    CreateTableRequest request = new CreateTableRequest(tableMeta, tableOptions, indexMetas);

    client.createTable(request);
}