Cassandra 二维数据建模

Cassandra two dimensional data modelling

用例:

对于一场比赛,我正在收集每场比赛的结果。始终是 A 队对战 B 队。每队由 5 名选手组成,每队选出一名冠军,一场比赛的可能结果是一方获胜/失败或两队平局。

我想找出最佳冠军组合我想根据每支球队选择的冠军组合创建 win/lose/draw 统计数据。总共有大约 100 个冠军可供玩家选择。所以有很多不同的冠军组合可能。

更多(奖励)功能:

我的问题:

我想知道根据英雄组合收集统计数据的最佳方法是什么?数据建模会是什么样子?

我的想法:

  1. 创建组合中所有 championId 的散列,字面上表示 championCombinationId,这是团队使用的冠军组合的唯一标识符。

  2. 创建一个二维 table 允许跟踪组合与组合统计数据。是这样的:

时间框架(每日日期)和 combinationId 的实际 championId 在那里丢失。

我尝试自己为上述要求创建模型,但我完全不确定。我也不知道我需要指定哪些键。

CREATE TABLE team_combination_statistics (
  combinationIdA text, // Team A
  combinationIdB text, // Team B
  championIdsA text, // An array of all champion IDs of combination A
  championIdsB text, // An array of all champion IDs of combination B
  trackingTimeFrame text, // A date?
  wins int,
  losses int,
  draws int
);

您可以创建一个统计数据 table 来保存某个冠军在指定日期的比赛数据。

CREATE TABLE champion_stats_by_day (
    champion_ids FROZEN<SET<INT>>,
    competing_champion_ids FROZEN<SET<INT>>,
    competition_day DATE,
    win_ratio DECIMAL,
    loss_ratio DECIMAL,
    draw_ratio DECIMAL,
    wins INT,
    draws INT,
    losses INT,
    matches INT,
    PRIMARY KEY(champion_ids, competition_day, competing_champion_ids)
) WITH CLUSTERING ORDER BY(competition_day DESC, competing_champion_ids ASC);

您可以从某个日期开始查询冠军的统计数据,但您必须在客户端中进行排序/聚合:

SELECT * FROM champion_stats_by_day WHERE champion_ids = {1,2,3,4} AND competition_day > '2017-10-17';

 champion_ids | competition_day | competing_champion_ids | draw_ratio | draws | loss_ratio | losses | matches | win_ratio | wins
--------------+-----------------+------------------------+------------+-------+------------+--------+---------+-----------+------
 {1, 2, 3, 4} |      2017-11-01 |         {2, 9, 21, 33} |       0.04 |     4 |       0.57 |     48 |      84 |      0.38 |   32
 {1, 2, 3, 4} |      2017-11-01 |         {5, 6, 22, 32} |      0.008 |     2 |       0.55 |    128 |     229 |      0.43 |   99
 {1, 2, 3, 4} |      2017-11-01 |       {12, 21, 33, 55} |       0.04 |     4 |       0.57 |     48 |      84 |      0.38 |   32
 {1, 2, 3, 4} |      2017-10-29 |         {3, 8, 21, 42} |          0 |     0 |      0.992 |    128 |     129 |     0.007 |    1
 {1, 2, 3, 4} |      2017-10-28 |         {2, 9, 21, 33} |       0.23 |    40 |       0.04 |      8 |     169 |      0.71 |  121
 {1, 2, 3, 4} |      2017-10-22 |        {7, 12, 23, 44} |       0.57 |    64 |       0.02 |      3 |     112 |       0.4 |   45

更新和插入工作如下。您首先 select 该日期和冠军 ID 的现有统计数据,然后进行更新。万一,当行不在 table 中时,这不会成为 Cassandra 执行的问题,在这种情况下 UPSERT。:

SELECT * FROM champion_stats_by_day WHERE champion_ids = {1,2,3,4} AND competing_champion_ids = {21,2,9,33} AND competition_day = '2017-11-01';
UPDATE champion_stats_by_day
    SET win_ratio = 0.38, draw_ratio = 0.04, loss_ratio = 0.57, wins = 32, draws = 4, losses = 48, matches = 84
    WHERE champion_ids = {1,2,3,4}
    AND competing_champion_ids = {21,2,9,33} 
    AND competition_day = '2017-11-01';

我还添加了示例 CQL 命令 here。 让我知道你的想法。

这个问题很长,所以在提出我的方法之前我会先讨论不同的主题,请准备好回答一个很长的问题:

  1. 数据规范化
  2. 具有相同值轴的二维tables

数据规范化

存储总数据量很有用,但按它排序不是,因为顺序不能确定一个组合是否好于另一个组合,它决定了大多数时候 won/lost 与相反,但游戏的总数量也很重要。

排序结果时,您希望按前两项的胜率、平局率、松率排序,因为第三项是线性组合。

具有相同值轴的二维tables

二维 table 的问题,其中两个维度代表相同的数据,在这种情况下是一组 5 个冠军,要么你制作一个三角形 table 要么你有数据加倍,因为你必须存储 cominationA 与 combinationB 和 combinationB 与 combinationA,因为 combinationX 是一组特定的 5 个冠军。

这里有两种方法,使用三角table或者手动加倍数据:

1。三角形 tables:

您创建一个 table,其中右上半部分为空或左下半部分为空。然后你在应用程序中处理哪个哈希是 A,哪个是 B,你可能需要交换它们的顺序,因为没有重复的数据。例如,您可以考虑字母顺序,其中 A < B 总是。如果您随后以错误的顺序请求数据,您将得不到任何数据。另一种选择是同时进行 A vs B 和 B vs A 查询,然后加入结果(显然交换输赢)。

2。手动加倍数据:

通过使用反映值(A、B、胜、平、输 & B、A、输、平、胜)进行两次插入,您将复制数据。这使您可以按任何顺序查询,但代价是使用两倍 space 并需要两次插入。

优缺点:

一种方法的优点是另一种方法的缺点。

三角形的优点tables

  • 不存储重复数据
  • 需要一半的插入

数据加倍的优点

  • 应用程序不关心您以何种顺序发出请求

我可能会使用三角 table 方法,因为应用程序复杂性的增加并没有那么重要,但可扩展性确实很重要。

提议的架构

使用你想要的任何键space,我从Whosebug中选择。根据需要修改复制策略或因子。

CREATE KEYSPACE so WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};

冠军名字table

冠军 table 将包含有关不同冠军的信息,目前它只会保存名称,但您可以在将来存储其他内容。

CREATE TABLE so.champions (
    c boolean,
    id smallint,
    name text,
    PRIMARY KEY(c, id)
) WITH comment='Champion names';

A boolean 用作分区键,因为我们希望将所有冠军存储在单个分区中以提高查询性能,并且记录数量较少(~100)我们将始终使用 c=Trueid 选择了 smallint,因为 2^7 = 128 是为了接近冠军的实际数量,并为未来的冠军留出空间而不使用负数。

查询冠军时,您可以通过以下方式获得所有冠军:

SELECT id, name FROM so.champions WHERE c=True;

或通过以下方式请求特定的:

SELECT name FROM so.champions WHERE c=True and id=XX;

历史比赛结果table

此 table 将存储匹配结果而不汇总:

CREATE TABLE so.matches (
    dt date,
    ts time,
    id XXXXXXXX,
    teams list<frozen<set<smallint>>>,
    winA boolean,
    winB boolean,
    PRIMARY KEY(dt, ts, id)
) WITH comment='Match results';

对于一个历史数据的分区table,而且正如你所说的每日精度,date似乎是一个不错的分区键。 time 列用作第一个聚类键,用于排序原因和完成时间戳,不管这些时间戳属于结束时刻还是结束时刻,选择一个并坚持下去。聚类键中需要一个额外的标识符,因为 2 场比赛可能会在同一时刻结束(时间具有纳秒精度,这基本上意味着重叠丢失的数据将非常微不足道,但您的数据源可能没有这种精度,因此最后一个关键列是必要的)。您可以为该列使用您想要的任何类型,您可能已经拥有一些标识符之王,其中包含您可以在此处使用的数据。您还可以选择一个随机数、一个由应用程序管理的增量整数,甚至是第一位玩家的名字,因为您可以确定同一位玩家不会 start/finish 在同一秒内玩两场游戏。

teams 列是最重要的一列:它存储了游戏中使用过的英雄的 ID。使用两个元素的序列,每个团队一个。内部(冻结)集用于每个团队中的 champs id,例如:{1,3,5,7,9}。我尝试了几个不同的选项:set< frozen<set<smallint>> >tuple< set<smallint>> , set<smallint> >list< frozen<set<smallint>> >。第一个选项不存储团队的顺序,因此我们无法知道谁赢得了比赛。第二个不接受在此列上使用索引并通过 CONTAINS 进行部分搜索,所以我选择了第三个保留顺序并允许部分搜索的方法。

另外两个值是两个布尔值,代表谁赢得了比赛。你可以有额外的列,比如 draw boolean 一列,但这个不是必需的,或者如果你想存储游戏的长度,则 duration time (我不是故意使用 Cassandra 的 duration 类型因为它只有在花费数月或至少数天的时间时才有价值),end timestamp/start timestamp 如果你想在分区和集群键等中存储你不使用的那个

部分搜索

为团队创建索引可能会有用,这样您就可以查询此列:

CREATE INDEX matchesByTeams ON so.matches( teams );

然后我们可以执行下面的SELECT语句:

SELECT * FROM so.matches WHERE teams CONTAINS {1,3,5,7,9};
SELECT * FROM so.matches WHERE teams CONTAINS {1,3,5,7,9} AND dt=toDate(now());

第一个将 select 任何球队 select 编辑该组合的比赛,第二个将进一步过滤到今天的比赛。

统计缓存table

有了这两个table你就可以掌握所有的信息,然后请求你需要的数据来计算相关的统计数据。一旦你计算了一些数据,你可以将这些信息作为一个 "cache" 存储回 Cassandra 中,在一个额外的 table 中,这样当用户请求显示一些统计数据时,你首先检查它们是否已经计算并且如果他们没有计算。 table 需要为用户可以输入的每个参数设置一列,例如:冠军组成、开始日期、结束日期、敌方队伍;以及统计信息本身的附加列。

CREATE TABLE so.stats (
    team frozen<set<smallint>>,
    s_ts timestamp,
    e_ts timestamp,
    enemy frozen<set<smallint>>,
    win_ratio float,
    loose_ratio float,
    wins int,
    draws int,
    looses int,
    PRIMARY KEY(team, s_ts, e_ts, enemy)
) WITH comment="Already calculated queries";

按 win/loose 比率排序:

要按比率而不是敌方队伍获取结果顺序,您可以使用物化视图。

CREATE MATERIALIZED VIEW so.statsByWinRatio AS
    SELECT * FROM so.stats
    WHERE team IS NOT NULL AND s_ts IS NOT NULL AND e_ts IS NOT NULL AND win_ratio IS NOT NULL AND enemy IS NOT NULL
    PRIMARY KEY(team, s_ts, e_ts, win_ratio, enemy)
    WITH comment='Allow ordering by win ratio';

注意: 当我回答时,我意识到在数据库中引入 "patch" 的概念,这样用户就不能确定日期,但补丁可能是更好的解决方案。如果您有兴趣发表评论,我将编辑答案以包含补丁概念。这意味着要稍微修改 so.historicso.stats table,但变化很小。