| <!-- |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| |
| --> |
| |
| # 按时间倒序查询 |
| |
| ## 实现原理 |
| |
| 倒序分为 3 个维度来实现,由内核层到用户层分别为: `TsFile`、`BatchData`、`数据集`. |
| 1. `TsFile` 是用来存储所有原始数据的文件格式,在存储时分为 `顺序文件` 和 `乱序文件`. |
| `ChunkData` 是 `TsFile` 中的基本数据块,保存了具体某一个测点的数据,且**数据是按照时间升序存储的**. |
| 2. `BatchData` 是基础数据交换结构,无论是从文件系统还是缓存读出的数据都会被转换成 `BatchData` 结构. |
| 3. `数据集` 是封装根据用户输入的 `SQL` 语句的结果集格式,转换和封装 `BatchData` 中的数据。 |
| |
| 实际系统需要考虑的方面会更多,但主要为以上 3 点。 |
| 下面分别介绍各个层面如何实现基于时间的倒序查询: |
| |
| ## TsFile |
| 1.`TsFile` 在文件末尾存储了各个测点在文件中的最小时间(开始时间)和最大时间(结束时间), |
| 所以当我们按照开始时间升序排列文件并读取数据时,是升序读取;当使用结束时间倒序排列文件 |
| (**顺序文件也可以使用开始时间倒序排列**)并读取数据时,是**伪倒序读取**(因为文件内依然是升序存储的),数据示例: |
| |
| ``` |
| 按照开始时间升序排列 (1,6) |
| +---TsFile1---+ +---TsFile2---+ |
| | 1,2,3,4,5 | | 6,7,8,9,10 | |
| +-------------+ +-------------+ |
| |
| 按照结束降序排列 (10,5) |
| +---TsFile2---+ +---TsFile1---+ |
| | 6,7,8,9,10 | | 1,2,3,4,5 | |
| +-------------+ +-------------+ |
| ``` |
| |
| 2.上面的示例只描述了顺序文件,乱序文件的特点是: 文件中存储的数据与其他文件的数据存在相交, |
| 不仅仅和顺序文件相交,乱序和乱序文件之间也可能相交。 |
| |
| ``` |
| +---TsFile1---+ +---TsFile2---+ |
| | 1,2,3,4,5 | | 6,7,8,9,10 | |
| +-------------+ +-------------+ |
| +--------------unseq1-----------+ |
| | 2,11 | |
| +-------------------------------+ |
| +-unseq2-+ |
| | 3,4,5 | |
| +--------+ |
| |
| ``` |
| |
| 3.对于**相交**的部分,需要使用`多路归并`查询出最终结果。具体处理细节可以参见[SeriesReader](../DataQuery/SeriesReader.md). |
| |
| 4.对于文件层而言,我们抽象出了`TimeOrderUtils`,用来完成对升序、降序不同实现的方法提取。 |
| |
| ```java |
| public interface TimeOrderUtils { |
| //排序使用的字段,升序使用开始时间,降序使用结束时间。 |
| //TsFileResource面对文件相关,Statistics面向Chunk和Page相关 |
| long getOrderTime(Statistics statistics); |
| long getOrderTime(TsFileResource fileResource); |
| |
| //判断是否相交使用的时间依据,升序使用结束时间,降序使用开始时间 |
| long getOverlapCheckTime(Statistics range); |
| |
| //根据提供的时间判断是否相交,left为依据时间,right为要判断的文件 |
| boolean isOverlapped(Statistics left, Statistics right); |
| boolean isOverlapped(long time, Statistics right); |
| boolean isOverlapped(long time, TsFileResource right); |
| |
| //对于有序文件的遍历规则,升序时正序遍历,降序时倒序遍历 |
| TsFileResource getNextSeqFileResource(List<TsFileResource> seqResources, boolean isDelete); |
| |
| //对于所有文件层的临时缓存排序规则的实现 |
| <T> Comparator<T> comparingLong(ToLongFunction<? super T> keyExtractor); |
| |
| //循环读取数据时判断是否到达了当前设置的停止点 |
| boolean isExcessEndpoint(long time, long endpointTime); |
| |
| //或许当前使用的排序方式 |
| boolean getAscending(); |
| } |
| ``` |
| |
| 5.完成了上述步骤,倒序取出来的数据如下: |
| |
| ``` |
| 6,7,8,9,10,1,2,3,4,5 |
| ``` |
| |
| 这是因为数据在Chunk、Page、缓存中都是升序存储,所以转移到 `BatchData` 中查看就是示例中的样子。 |
| |
| ## BatchData |
| 1.为了兼容原有的文件读取数据方式,所以在数据放到 `BatchData` 之后倒序读取集合里的数据就可以得到最终结果。 |
| |
| BatchData类似一个二维数组的格式,封装了写入和读取的方法,对于倒序,抽象了下面的子类: |
| ```java |
| |
| public class DescBatchData extends BatchData { |
| //判断是否还有值,正序时候判断是否读到写入的size位置,倒序就是判断是否读到了0的位置 |
| public boolean hasCurrent(); |
| //指针移动到下一个数据,正序为 index+1 操作, 倒序为 index-1 操作 |
| public void next(); |
| //重制指针位置,正序为重制到0,倒序重制到数据写入位置 |
| public void resetBatchData(); |
| //就像是Nio中的Buffer类的flip操作,在数据写入完成时做的操作,为读取做好准备 |
| public BatchData flip(); |
| //使用具体时间获取值,正序时index从0到size遍历,倒序时从size到0遍历 |
| public Object getValueInTimestamp(long time); |
| } |
| ``` |
| |
| 2.在需要倒序处理数据的位置构建`DescBatchData`类,为此,特意设计了一个`BatchDataFactory`类, |
| 根据ascending参数判断生产哪个类型的BatchData。 |
| |
| 3.因为`MergeReader`使用了`PageReader`,所以在降序模式的时候数据会被倒序读出来(如下): |
| ``` |
| 5 4 3 2 1 |
| ``` |
| 这种情况会使得`BatchData`使用`getValueInTimestamp`方法变得困难(因为在正序模式的时候默认为越往后的数据时间越大, |
| 倒序模式的时候越后时间越小),但是按照如上数据示例无论在哪种模式下,都是违反了期望的数据时间比较,所以,新增了一个 |
| `getValueInTimestamp(long time, BiFunction<Long, Long, Boolean> compare)`方法, |
| 让不同的使用场景抉择基于时间的数据查找方式。 |
| |
| 4.至此数据从磁盘和缓存中取出的数据都是严格按照时间排序的了。 |
| |
| ## 数据集 |
| 1.原始数据查询 |
| ```sql |
| select * from root order by time desc |
| select * from root where root.ln.d1.s1>100 order by time desc |
| select * from root where time >100 order by time desc |
| ``` |
| 原始数据查询中,只要向`SeriesReader`传递了正确的ascending参数就能得到正确的结果。 |
| |
| 2.GroupBy |
| ```sql |
| select max_time(*) from root group by ((0,10],2ms) order by time desc |
| ``` |
| 如上所示的Sql中,groupBy查询的区间分别为: |
| ``` |
| [0-2),[2-4),[4-6),[6-8),[8-10) |
| ``` |
| 倒序查询只需要将时间计算的过程修改为: |
| ``` |
| [8-10),[6-8),[4-6),[2-4),[0-2) |
| ``` |
| |
| >计算方法为: |
| > |
| > ((结束时间-开始时间)/步长值) 结果向上取整,得到的值为GroupBy中会发生的遍历次数(记为i)。 |
| > |
| > (步长 * (i-1) + 开始时间) 结果为第i次遍历的开始时间值。 |
| |
| |
| 对于BatchData中的数据大于每次遍历的结束时间时,进行skip,然后对 `AggregateResult` 新增 |
| minBound计算限制,限制为第i次遍历的开始时间值, |
| **数据无论是正序还是倒序遍历对于`AggregateResult`的结果是等价的**. |
| |
| 3.GroupByFill |
| ```sql |
| select last_value(s0) from root.ln.d1 group by ((0,5],1ms) fill (int32[Previous]) order by time desc |
| ``` |
| |
| 对于升序的插值计算, 在 `GroupByFillDataSet` 中做了前值缓存,当本次查询的值不为空时,更新缓存; |
| 为空时使用缓存中的数据。 |
| |
| 对于降序查询,因为数据在倒序遍历,并不知道它的前值是多少,所以为 `GroupByEngineDataSet` |
| 抽象了 `peekNextNotNullValue` 方法,继续往前执行,一直查找到一个不为空的值返回。并将该值缓存到 |
| `GroupByFillDataSet` 中并遵从升序的补值方法,到当前的循环开始时间小于缓存的时间时缓存失效。 |
| |
| 4.Last |
| 在`LastQueryExecutor`中,一次性将所有的结果集封装在了一个List中, |
| 当倒序时将List中的数据根据时间进行一次排序,就得到了结果,详情见`ListDataSet`. |