C# で DynamoDB から時系列データを GSI を使って範囲指定して検索する


C# で Amazon DynamoDB に保存されている時系列データを GSI(グローバルセカンダリインデックス)を使って範囲指定して検索する方法です。

[事前準備1] データ

下記の構造のデータを保存できるよう、

[DynamoDBTable("SampleTable")]
public class Sample
{
    /// <summary>「SampleTable」テーブルのハッシュキー</summary>
    [DynamoDBHashKey]
    public string Id { get; set; }

    /// <summary>「SampleTable」テーブルのレンジキー</summary>
    [DynamoDBRangeKey]
    public string DateAndTime { get; set; }

    /// <summary>「Date-Time-index」GSI のハッシュキー</summary>
    [DynamoDBGlobalSecondaryIndexHashKey]
    public string Date { get; set; }

    /// <summary>「Date-Time-index」GSI のレンジキー</summary>
    [DynamoDBGlobalSecondaryIndexRangeKey]
    public string Time { get; set; }

    [DynamoDBProperty]
    public string Message { get; set; }

    public override string ToString()
    {
        return $"{Id} / {DateAndTime} / {Message}";
    }
}

AWS Console から IdDateAndTime をキーにした SampleTable テーブルを用意します。

次に、下記のように SaveAsync メソッドを使って 10件のデータを作成します。

// DynamoDB にアクセスするためのオブジェクトを用意
using var client = new AmazonDynamoDBClient(RegionEndpoint.APNortheast1);
using var context = new DynamoDBContext(client);

// 10件のデータを作成
for (var i = 0; i < 10; i++)
{
    var now = DateTime.Now;

    await context.SaveAsync(new Sample
    {
        Id = $"{i % 3 + 1:D3}",
        DateAndTime = $"{now:yyyy-MM-dd HH:mm:ss.fff}",
        Date = $"{now:yyyy-MM-dd}",
        Time = $"{now:HH:mm:ss}",
        Message = $"メッセージ - {i + 1}"
    });

    // 保存される時間をずらしたいので 500 msec 待機
    await Task.Delay(500);
}

下記のように ScanAsync メソッドを使って全データを取得します。

// 全データを取得
var listAll = await context.ScanAsync<Sample>(null).GetRemainingAsync();

Console.WriteLine("--- listAll");
listAll.OrderBy(n => n.DateAndTime).ToList().ForEach(Console.WriteLine);

そうすると、下記のようにデータを取得できます。

--- listAll
001 / 2020-08-11 11:40:40.236 / メッセージ - 1
002 / 2020-08-11 11:40:41.224 / メッセージ - 2
003 / 2020-08-11 11:40:41.756 / メッセージ - 3
001 / 2020-08-11 11:40:42.293 / メッセージ - 4
002 / 2020-08-11 11:40:42.824 / メッセージ - 5
003 / 2020-08-11 11:40:43.355 / メッセージ - 6
001 / 2020-08-11 11:40:43.877 / メッセージ - 7
002 / 2020-08-11 11:40:44.412 / メッセージ - 8
003 / 2020-08-11 11:40:44.936 / メッセージ - 9
001 / 2020-08-11 11:40:45.458 / メッセージ - 10

[事前準備2] GSI

AWS Console からは下記のようにデータが保存されていることを確認できます。

事前準備の最後に、日時を範囲指定して検索できるよう、AWS Console から DateTime をキーにした Date-Time-index インデックス(GSI)を用意します。

これで準備は完了です。

[シナリオ1] Id = 001 のデータを取得したい

下記のようにハッシュキーに 001 を指定して QueryAsync() メソッドを使ってデータを取得します。

var list01 = await context.QueryAsync<Sample>("001").GetRemainingAsync();

Console.WriteLine("--- list01");
list01.OrderBy(n => n.DateAndTime).ToList().ForEach(Console.WriteLine);

そうすると、下記のようにデータを取得できます。

--- list01
001 / 2020-08-11 11:40:40.236 / メッセージ - 1
001 / 2020-08-11 11:40:42.293 / メッセージ - 4
001 / 2020-08-11 11:40:43.877 / メッセージ - 7
001 / 2020-08-11 11:40:45.458 / メッセージ - 10

Id が 001 のデータのみを取得できています。

[シナリオ2] 2020-08-11 の 11:40:42~11:40:43 のデータを取得したい

下記のように GSI の名称 Date-Time-index を指定し、ハッシュキーに 2020-08-11 を指定して、レンジキーには QueryOperator.Between11:40:42 - 11:40:43 を指定して QueryAsync() メソッドを使ってデータを取得します。

var list02 = await context.QueryAsync<Sample>(
    "2020-08-11",
    QueryOperator.Between,
    new List<object>
    {
        "11:40:42",
        "11:40:43"
    },
    new DynamoDBOperationConfig
    {
        IndexName = "Date-Time-index" // 利用する GSI を指定
    }).GetRemainingAsync();

Console.WriteLine("--- list02");
list02.OrderBy(n => n.DateAndTime).ToList().ForEach(Console.WriteLine);

そうすると、下記のようにデータを取得できます。

--- list02
001 / 2020-08-11 11:40:42.293 / メッセージ - 4
002 / 2020-08-11 11:40:42.824 / メッセージ - 5
003 / 2020-08-11 11:40:43.355 / メッセージ - 6
001 / 2020-08-11 11:40:43.877 / メッセージ - 7

SampleTable テーブルのハッシュキーである Id をまたいで 11:40:42 - 11:40:43 のデータのみを取得できています。

さいごに

GSI を使って日時を範囲指定して検索することはできました。
ただ、コスト面の考慮や NoSQL らしい設計についてはできているかというと、、

より良い方法があったら教えてください m(_ _)m