<!-- toc -->

<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<ins class="adsbygoogle"
     style="display:block; text-align:center;"
     data-ad-layout="in-article"
     data-ad-format="fluid"
     data-ad-client="ca-pub-8828078415045620"
     data-ad-slot="7586680510"></ins>
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script>

## 段
ApacheDruid将索引存储在按时间分区的*段文件*中。在基本设置中，通常为每个时间间隔创建一个段文件，其中时间间隔可在 `granularitySpec` 的`segmentGranularity` 参数中配置。为了使Druid在繁重的查询负载下运行良好，段文件大小必须在建议的300MB-700MB范围内。如果段文件大于此范围，请考虑更改时间间隔的粒度，或者对数据进行分区，并在 `partitionsSpec` 中调整 `targetPartitionSize`（此参数的建议起点是500万行）。有关更多信息，请参阅下面的**分片部分**和[批处理摄取](../DataIngestion/native.md)文档的**分区规范**部分。

### 段文件的核心数据结构

在这里，我们描述段文件的内部结构，它本质上是*列式*的：每列的数据在单独的数据结构中。通过分别存储每一列，Druid可以通过只扫描查询实际需要的列来减少查询延迟。有三种基本列类型：**时间戳列、维度列和指标列**，如下图所示：

![](img/druid-column-types.png)

timestamp和metric列很简单：在底层，每个列都是用LZ4压缩的整数或浮点值数组。一旦查询知道需要选择哪些行，它只需解压缩这些行，提取相关行，然后使用所需的聚合运算符进行计算。与所有列一样，如果不查询一个列，则跳过该列的数据。

dimension列是不同的，因为它们支持过滤和聚合操作，所以每一个维度都需要以下三种数据结构：

1. 一个将值（通常被当做字符串）映射到整数id的字典
2. 一个使用第一步的字典进行编码的列值的列表
3. 对于列中每一个不同的值，标识哪些行包含该值的位图

为什么需要这三种数据结构？字典简单地将字符串值映射到整数id，以便于在（2）和（3）中可以紧凑的表示。（3）中的位图（也称*倒排索引*）可以进行快速过滤操作（特别是，位图便于快速进行AND和OR操作）。 最后，`GroupBy` 和 `TopN` 查询需要（2）中的值列表。换句话说，仅基于过滤器的聚合指标是不需要（2）中存储的维度值列表的。

要具体了解这些数据结构，请考虑上面示例数据中的"page"列,表示此维度的三个数据结构如下图所示：

```json
1: Dictionary that encodes column values
  {
    "Justin Bieber": 0,
    "Ke$ha":         1
  }

2: Column data
  [0,
   0,
   1,
   1]

3: Bitmaps - one for each unique value of the column
  value="Justin Bieber": [1,1,0,0]
  value="Ke$ha":         [0,0,1,1]
```

注意，位图与前两个数据结构不同：前两个数据结构在数据大小上呈线性增长（在最坏的情况下），而位图部分的大小是数据大小 * 列基数的乘积。不过，压缩在这里会有帮助，因为我们知道对于"列数据"中的每一行，只有一个具有非零项的位图,这意味着高基数列将具有非常稀疏的、高度可压缩的位图。Druid使用特别适合位图的压缩算法（如Roaring位图压缩）来利用这一点特性。

**多值列**

如果数据源使用多值列，那么段文件中的数据结构看起来有点不同。让我们假设在上面的例子中，第二行同时标记了"Ke$ha"和"Justin Bieber"主题。在这种情况下，这三种数据结构现在看起来如下：

```json
1: Dictionary that encodes column values
  {
    "Justin Bieber": 0,
    "Ke$ha":         1
  }

2: Column data
  [0,
   [0,1],  <--Row value of multi-value column can have array of values
   1,
   1]

3: Bitmaps - one for each unique value
  value="Justin Bieber": [1,1,0,0]
  value="Ke$ha":         [0,1,1,1]
                            ^
                            |
                            |
    Multi-value column has multiple non-zero entries
```

请注意列数据和Ke$ha位图中第二行的更改。如果一行中的某一列有多个值，则其在“列数据”中的条目是一个数组。此外，在“列数据”中有n个值的行在位图中将有n个非零值项。

### SQL兼容的空值处理

默认情况下，Druid字符串维度列可以使用 `""` 或者 `null` ，数值列和指标列则不能表示为 `null`，而是将 `null` 强制为 `0`。但是，Druid还提供了一个与SQL兼容的空值处理模式，必须在系统级别通过 `Druid.generic.useDefaultValueForNull` 启用, 当设置为 `false` 时，此设置将允许Druid在接收数据时创建的段中：字符串列区分 `""` 和 `null` ，数值列区分 `null` 和 `0`。

在这种模式下，字符串维度列不包含额外的列结构，只是为 `null` 保留额外的字典条目。但是，数值列将与一个附加位图一起存储在段中，该位图标识哪些行是 `null` 值。除了略微增加段大小之外，由于需要检查 `null` 的位图，SQL兼容的空值处理在查询时也会导致性能损失，此性能开销仅对实际包含`null`列的场景中存在。

### 命名规则

段标识符通常使用数据源，时间区间的开始时间（ISO 8601格式），时间区间的结束时间（ISO 8601格式）和版本来构造。如果数据被额外的分片后超出了时间范围，则段标识符还将包含分区号。

一个示例段标识符可以是：
`数据源名称_开始时间_结束时间_版本号_分区号`

### 段的组成

在底层，一个段由以下几个文件组成：

* `version.bin`
  
  4个字节，以整数表示当前段版本。 例如，对于v9段，版本为0x0、0x0、0x0、0x9

* `meta.smoosh`
  
  一个包含其他 `smoosh` 文件内容的元数据(文件名以及偏移量)文件

* `XXXXX.smoosh`

  这些文件中有一些是串联的二进制数据

  `smoosh` 文件代表 "smooshed" 在一起的多个文件，以减少必须打开用来容纳数据的文件描述符的数量，它们是最大为2GB的文件（以匹配Java中内存映射的ByteBuffer的限制）。`smoosh` 文件为数据中的每个列提供单独的文件，并在 `index.drd` 文件提供有关该段的额外元数据。

  还有一个称为 `__time` 的特殊列，它表示该段的时间列。希望随着代码的发展，这种特殊性将越来越少，但就目前而言，它就像我妈妈一直告诉我的那样特殊。

在代码库中，段具有内部格式版本。当前的句段格式版本为 `v9`。

### 列的格式

每列存储为两部分：
1. Jackson序列化的列描述符
2. 列二进制文件的其余部分

**列描述符**本质上是一个对象，它允许我们使用Jackson的多态反序列化来添加新的有趣的序列化方法，并且对代码的影响最小。它由关于列的一些元数据（它是什么类型的，它是多值的，等等）和一列序列化/反序列化逻辑组成，这些逻辑可以反序列化二进制文件的其余部分。

### 切分数据以创建段
#### 数据分片

对于同一数据源，同一时间间隔内可能存在多个段。这些段在一段时间内形成一个 `块` 。根据用于切分数据的 `shardSpec` 的类型，只有当一个 `块` 完成时，Druid查询才能完成。也就是说，如果一个块由3个段组成，例如：

`sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_0`

`sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_1`

`sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_2`

在完成对间隔 `2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z` 的查询之前，必须加载所有3个段。

此规则的例外是使用**线性切片规范**。线性切片规范不会强制"完整性"，即使系统中没有加载切片，查询也可以完成。例如，如果您的实时摄取任务创建了3个使用线性切片规范进行分段的段，并且系统中只加载了其中的两个段，那么查询将只返回这两个段的结果。

### Schema更改
### 替换段

Druid使用数据源、间隔、版本和分区号唯一地标识段。只有在为某个时间粒度创建多个段时，分区号才在段id中可见。例如，如果有小时段，但一小时内的数据量超过单个段的容量，则可以为同一小时创建多个段。这些段将共享相同的数据源、间隔和版本，但具有线性增加的分区号。

```json
foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-01/2015-01-02_v1_1
foo_2015-01-01/2015-01-02_v1_2
```

在上述段的实例中，`dataSource = foo`, `interval = 2015-01-01/2015-01-02`, `version = v1`, and `partitionNum = 0`。 如果在以后的某个时间点，使用新的schema重新索引数据，则新创建的段将具有更高的版本id。

```json
foo_2015-01-01/2015-01-02_v2_0
foo_2015-01-01/2015-01-02_v2_1
foo_2015-01-01/2015-01-02_v2_2
```

Druid批量索引任务（基于Hadoop或基于IndexTask）保证了间隔内的的原子更新。在我们的例子中，在 `2015-01-01/2015-01-02` 的所有 `v2` 段加载到Druid集群之前，查询只使用 `v1` 段。加载完所有 `v2` 段并可查询后，所有查询都将忽略 `v1` 段并切换到 `v2` 段。不久之后，`v1` 段将从集群中卸载。

请注意，跨越多个段间隔的更新在每个间隔内都是原子的, 但是在整个更新过程中它们不是原子的。例如，您有如下段：

```json
foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-02/2015-01-03_v1_1
foo_2015-01-03/2015-01-04_v1_2
```

`v2` 段将在构建后立即加载到集群中，并在段重叠的时间段内替换 `v1` 段。在完全加载 `v2` 段之前，集群可能混合了 `v1` 和 `v2` 段。

```json
foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-02/2015-01-03_v2_1
foo_2015-01-03/2015-01-04_v1_2
```

在这种情况下，查询可能会命中 `v1` 和 `v2` 段的混合.

### 段之间的不同schemas

同一数据源的Druid段可能有不同的schema。如果一个字符串列（维度列）存在于一个段中而不是另一个段中，则涉及这两个段的查询仍然有效。对缺少维度的段的查询将表现为该维度只有空值。类似地，如果一个段有一个数值列（指标列），而另一个没有，那么查询缺少指标列的段通常会"做正确的事情", 在缺失的指标上做聚合操作也就是缺失的。