使用 hll() 和 tdigest()

假设要计算过去 7 天内每日不同用户的计数。 你可以每天运行一次 summarize dcount(user),并将时间跨度筛选为过去七天。 此方法效率较低,因为每次运行计算时与上一计算之间有六天的重叠。 你还可以计算每天的聚合,然后将这些聚合组合在一起。 此方法要求你“记住”最后六个结果,但效率要高得多。

如上所述的分区查询对于简单聚合(例如 count()sum())很容易。 它还可用于复杂聚合,例如 dcount()percentiles()。 本文说明了 Kusto 如何支持此类计算。

下面的示例展示了如何使用 hll/tdigest,表明在某些情况下使用这些命令是高效的:

重要

hllhll_ifhll_mergetdigesttdigest_merge 的结果是类型为 dynamic 的对象,然后可由其他函数(dcount_hllpercentile_tdigestpercentiles_array_tdigestpercentrank_tdigest)进行处理。 此对象的编码可能会随时间而变化(例如,由于软件升级);但是,此类更改将以向后兼容的方式完成,因此可以持久存储此类值,并可靠地在查询中引用这些值。

注意

在某些情况下,hlltdigest 聚合函数生成的动态对象可能会很大,会超出编码策略中的默认 MaxValueSize 属性值。 如果是这样,则该对象将作为 null 引入。 例如,在准确度为 4 的情况下保存 hll 函数的输出时,hll 对象的大小超出了默认的 MaxValueSize (1MB)。 若要避免此问题,请修改列的编码策略,如下例所示。

range x from 1 to 1000000 step 1
| summarize hll(x,4)
| project sizeInMb = estimate_data_size(hll_x) / pow(1024,2)

输出

sizeInMb
1.0000524520874

在应用此类策略之前将此对象引入到表中会引入 null:

.set-or-append MyTable <| range x from 1 to 1000000 step 1
| summarize hll(x,4)
MyTable
| project isempty(hll_x)

输出

Column1
1

若要避免引入 null,请使用特殊编码策略类型 bigobject,它将 MaxValueSize 重写为 2 MB,如下所示:

.alter column MyTable.hll_x policy encoding type='bigobject'

现在将值引入到上面的同一个表中:

.set-or-append MyTable <| range x from 1 to 1000000 step 1
| summarize hll(x,4)

成功引入第二个值:

MyTable
| project isempty(hll_x)

输出

Column1
1
0

示例:使用装箱时间戳计数

这里有一个表 (PageViewsHllTDigest),其中包含每小时查看的页面的 hll 值。 你希望将这些值分段成 12h。 使用 hll_merge() 聚合函数合并 hll 值,将时间戳分段成 12h。 使用函数 dcount_hll 返回最终的 dcount 值:

PageViewsHllTDigest
| summarize merged_hll = hll_merge(hllPage) by bin(Timestamp, 12h)
| project Timestamp , dcount_hll(merged_hll)

输出

Timestamp dcount_hll_merged_hll
2016-05-01 12:00:00.0000000 20056275
2016-05-02 00:00:00.0000000 38797623
2016-05-02 12:00:00.0000000 39316056
2016-05-03 00:00:00.0000000 13685621

将时间戳分段成 1d

PageViewsHllTDigest
| summarize merged_hll = hll_merge(hllPage) by bin(Timestamp, 1d)
| project Timestamp , dcount_hll(merged_hll)

输出

Timestamp dcount_hll_merged_hll
2016-05-01 00:00:00.0000000 20056275
2016-05-02 00:00:00.0000000 64135183
2016-05-03 00:00:00.0000000 13685621

可以对 tdigest 的值执行同一查询,这些值表示每小时的 BytesDelivered

PageViewsHllTDigest
| summarize merged_tdigests = merge_tdigest(tdigestBytesDel) by bin(Timestamp, 12h)
| project Timestamp , percentile_tdigest(merged_tdigests, 95, typeof(long))

输出

Timestamp percentile_tdigest_merged_tdigests
2016-05-01 12:00:00.0000000 170200
2016-05-02 00:00:00.0000000 152975
2016-05-02 12:00:00.0000000 181315
2016-05-03 00:00:00.0000000 146817

示例:临时表

如果数据集太大,则会达到 Kusto 限制。在这种情况下,你需要对数据集运行定期查询,但需要运行常规查询来针对大型数据集计算 percentile()dcount()

若要解决此问题,可以将新增加的数据作为 hlltdigest 值添加到一个临时表中,当所需的运算为 dcounttdigest() 时,请使用 hll();当所需的运算为 percentile 时,请使用 set/appendupdate policy。 在这种情况下,dcounttdigest 的中间结果会保存到另一个数据集中,该数据集应小于大的目标数据集。

若要解决此问题,可以将新增加的数据作为 hlltdigest 值添加到一个临时表中,当所需的运算为 dcount 时,请使用 hll()。 在这种情况下,dcount 的中间结果保存到另一个数据集中,该数据集应小于大的目标数据集。

当需要获取这些值的最终结果时,可以让查询使用 hll/tdigest 合并:hll-merge()/tdigest_merge()。 然后,在获取合并值之后,可以对这些合并值调用 percentile_tdigest() / dcount_hll(),以获取 dcount 或 percentile 运算的最终结果。

假设有一个表 PageViews,每天向其中引入数据。在 date = datetime(2016-05-01 18:00:00.0000000) 过后,你每天都要计算每分钟查看的页面的非重复计数。

运行以下查询:

PageViews
| where Timestamp > datetime(2016-05-01 18:00:00.0000000)
| summarize percentile(BytesDelivered, 90), dcount(Page,2) by bin(Timestamp, 1d)

输出

Timestamp percentile_BytesDelivered_90 dcount_Page
2016-05-01 00:00:00.0000000 83634 20056275
2016-05-02 00:00:00.0000000 82770 64135183
2016-05-03 00:00:00.0000000 72920 13685621

你每次运行此查询时(例如,你希望一天运行多次此查询),此查询都会聚合所有值。

如果你使用更新策略或 set/append 命令将 hlltdigest 值(这些值是 dcount 和 percentile 运算的中间结果)保存到临时表 PageViewsHllTDigest 中,则可以仅合并这些值,然后通过以下查询使用 dcount_hll/percentile_tdigest

PageViewsHllTDigest
| summarize  percentile_tdigest(merge_tdigest(tdigestBytesDel), 90), dcount_hll(hll_merge(hllPage)) by bin(Timestamp, 1d)

输出

Timestamp percentile_tdigest_merge_tdigests_tdigestBytesDel dcount_hll_hll_merge_hllPage
2016-05-01 00:00:00.0000000 84224 20056275
2016-05-02 00:00:00.0000000 83486 64135183
2016-05-03 00:00:00.0000000 72247 13685621

此查询的性能应当更高,因为它针对更小的表运行。 在此示例中,第一个查询针对大约 215M 记录运行,而第二个查询仅针对 32 条记录运行:

示例:中间结果

保留查询。 假设你有一个表,其中汇总了用户查看每个维基百科页面的时间(样本大小为 10M),并且对于每个 date1 date2,你想要查明在 date1 和 date2 这两天查看的页面数相对于在 date1 (date1 < date2) 这天查看的页面数的比率。

琐细的方法是使用 join 和 summarize 运算符:

// Get the total pages viewed each day
let totalPagesPerDay = PageViewsSample
| summarize by Page, Day = startofday(Timestamp)
| summarize count() by Day;
// Join the table to itself to get a grid where 
// each row shows foreach page1, in which two dates
// it was viewed.
// Then count the pages between each two dates to
// get how many pages were viewed between date1 and date2.
PageViewsSample
| summarize by Page, Day1 = startofday(Timestamp)
| join kind = inner
(
    PageViewsSample
    | summarize by Page, Day2 = startofday(Timestamp)
)
on Page
| where Day2 > Day1
| summarize count() by Day1, Day2
| join kind = inner
    totalPagesPerDay
on $left.Day1 == $right.Day
| project Day1, Day2, Percentage = count_*100.0/count_1

输出

第 1 天 第 2 天 百分比
2016-05-01 00:00:00.0000000 2016-05-02 00:00:00.0000000 34.0645725975255
2016-05-01 00:00:00.0000000 2016-05-03 00:00:00.0000000 16.618368960101
2016-05-02 00:00:00.0000000 2016-05-03 00:00:00.0000000 14.6291376489636

以上查询用了大约 18 秒的时间。

使用 hll()hll_merge()dcount_hll() 函数时,等效的查询将在大约 1.3 秒后结束,这表明 hll 函数将上述查询的速度提高了大约 13 倍:

let Stats=PageViewsSample | summarize pagehll=hll(Page, 2) by day=startofday(Timestamp); // saving the hll values (intermediate results of the dcount values)
let day0=toscalar(Stats | summarize min(day)); // finding the min date over all dates.
let dayn=toscalar(Stats | summarize max(day)); // finding the max date over all dates.
let daycount=tolong((dayn-day0)/1d); // finding the range between max and min
Stats
| project idx=tolong((day-day0)/1d), day, pagehll
| mv-expand pidx=range(0, daycount) to typeof(long)
// Extend the column to get the dcount value from hll'ed values for each date (same as totalPagesPerDay from the above query)
| extend key1=iff(idx < pidx, idx, pidx), key2=iff(idx < pidx, pidx, idx), pages=dcount_hll(pagehll)
// For each two dates, merge the hll'ed values to get the total dcount over each two dates, 
// This helps to get the pages viewed in both date1 and date2 (see the description below about the intersection_size)
| summarize (day1, pages1)=arg_min(day, pages), (day2, pages2)=arg_max(day, pages), union_size=dcount_hll(hll_merge(pagehll)) by key1, key2
| where day2 > day1
// To get pages viewed in date1 and also date2, look at the merged dcount of date1 and date2, subtract it from pages of date1 + pages on date2.
| project pages1, day1,day2, intersection_size=(pages1 + pages2 - union_size)
| project day1, day2, Percentage = intersection_size*100.0 / pages1

输出

day1 day2 百分比
2016-05-01 00:00:00.0000000 2016-05-02 00:00:00.0000000 33.2298494510578
2016-05-01 00:00:00.0000000 2016-05-03 00:00:00.0000000 16.9773830213667
2016-05-02 00:00:00.0000000 2016-05-03 00:00:00.0000000 14.5160020350006

注意

由于 hll 函数的误差,查询结果不是 100% 准确。 有关误差的详细信息,请参阅 dcount()