一、位图(Bitmap)

1、什么是比特(bit)
      1)它是英文 binary digit 的缩写
      2)它是计算机内部存储的最小单位,用二进制的0或者1来表示 
      3)1 Byte = 8 bit;1024 Byte = 1 Kb;1024 Kb = 1 Mb;1024 Mb = 1 Gb;1024 Gb = 1 Tb

2、引子

给出40亿个连续不重复且无序的无符号int型整数,目前条件是只有一个2G内存的PC,需要判断出某个数字是否在给出的这40亿个数字里面

分析:int占4个Byte,40亿 * 4 / 1024 / 1024 / 1024 ≈ 14.9 Gb,目前内存只有2G根本不满足要求(需要注意的是 int 无符号最大值是 4294967295,二进制的最高位为符号位),此时就需要位图(Bitmap)来处理了

1)什么是位图(Bitmap)
        a)位图(Bitmap)就是用一个bit位表示数字。从0开始,第N位的bit位表示整数N。bit位为1表示该整数存在,bit位为0表示整数不存在


        b)位图本质上就是一个数组
        c)位图采用的空间换时间的方式来提高计算的效率
   2)通过位图(Bitmap)解决后:40亿个数字如果我们用40亿个bit来表示,则需要占据的空间为 40亿 / 8 / 1024 / 1024 ≈ 476.83 Mb,大大降低了内存的消耗

3、缺点:以上是在数据连续的情况下占用了476.83Mb,假如现在只存第40亿一个数那仍然会占476.83Mb的内存。也就是说在数据密集的时候使用位图是很划算的,如果数据稀疏那就不划算了

二、压缩位图(RoaringBitmap)

1、实现原理
     1)压缩位图(RoaringBitmap,以下简称RBM)处理的是无符号int类型的整数
     2)RBM将一个32位的int拆分为高16位与低16位分开去处理,其中高16位作为索引,低16位作为实际存储数据


2、数据结构
     1)RoaringBitmap

RoaringArray highLowContainer = null;
/*** Create an empty bitmap*/
public RoaringBitmap() {highLowContainer = new RoaringArray();
}

2)RoaringArray

static final int INITIAL_CAPACITY = 4;
// short占2个字节,16位,一个short正好可以表示int高16位的所有数值
short[] keys = null;
// Container用来存储int低16位的2^16个int类型的整数
Container[] values = null;
protected RoaringArray() {this.keys = new short[INITIAL_CAPACITY];this.values = new Container[INITIAL_CAPACITY];
}

3)Container

a)ArrayContainer

// ArrayContainer中允许的最大数据量
// 4096 * 2Byte / 1024 = 8k 也就是说ArrayContainer最大容量时所占的内存为8k
static final int DEFAULT_MAX_SIZE = 4096;// containers with DEFAULT_MAX_SIZE or less integers should be ArrayContainers
// 基数(元素个数)
protected int cardinality = 0;
// 用来存储int类型低16位的整数,也就是说ArrayContainer中存储的数字来自0~65535(2^16-1),且只能存这个范围内的4096个数
short[] content;

b)BitmapContainer

// 最大可以存储2^16个比特位(每个bit对应一个数值, 最大可以表示2^16个int类型的整数)
protected static final int MAX_CAPACITY = 1 << 16;
long[] bitmap;
int cardinality;
public BitmapContainer() {this.cardinality = 0;// long占8Byte(64bit)// 2^16bit / 64bit = 1024 也就是说需要1024个long, 所以此处new一个长度为1024长度的long数组// 2^16bit / 8 / 1024 = 8Kb (1024个long * 8Byte / 1024 = 8Kb), 所以BitmapContainer始终占据内存空间为8Kbthis.bitmap = new long[MAX_CAPACITY / 64];
}
ArrayContainer 与 BitmapContainer 随着存储的数据量增多时所占内存空间对比图

c)RunContainer

private short[] valueslength;// we interleave values and lengths, so
// that if you have the values 11,12,13,14,15, you store that as 11,4 where 4 means that beyond 11 itself, there are
// 4 contiguous values that follows.
// Other example: e.g., 1, 10, 20,0, 31,2 would be a concise representation of  1, 2, ..., 11, 20, 31, 32, 33
int nbrruns = 0;// how many runs, this number should fit in 16 bits.
private RunContainer(int nbrruns, short[] valueslength) {this.nbrruns = nbrruns;this.valueslength = Arrays.copyOf(valueslength, valueslength.length);
}

有关RunContainer的注意事项:

在RBM创立初期只有以上两种容器,RunContainer其实是在后期加入的。RunContainer是基于之前提到的RLE算法进行压缩的,主要解决了大量连续数据的问题。
举例说明:3,4,5,10,20,21,22,23这样一组数据会被优化成3,2,10,0,20,3,原理很简单,就是记录初始数字以及连续的数量,并把压缩后的数据记录在short数组中
显而易见,这种压缩方式对于数据的疏密程度非常敏感,举两个最极端的例子:如果这个Container中所有数据都是连续的,也就是[0,1,2.....65535],压缩后为0,65535,即2个short,4字节。若这个Container中所有数据都是间断的(都是偶数或奇数),也就是[0,2,4,6....65532,65534],压缩后为0,0,2,0.....65534,0,这不仅没有压缩反而膨胀了一倍,65536个short,即128kb
因此是否选择RunContainer是需要判断的,RBM提供了一个转化方法runOptimize()用于对比和其他两种Container的空间大小,若占据优势则会进行转化

3、数据存储使用示例

1)代码

RoaringBitmap rbm = new RoaringBitmap();
// 从 0 到 2^16-1, 这个范围内的高位索引为0, 此处取了 5 个数
rbm.add(0);
rbm.add(1);
rbm.add(10);
rbm.add(10000);
rbm.add(65335);
// 从 2^16 到 2^17-1, 这个范围内的高位索引是1, 此处取了 2^15 个偶数
for (int i = 65536; i < 65536 * 2; i+=2) {rbm.add(i);
}
// 其实就是从 2^17+2^16 到 2^17+2^17-1 这个范围内的高位索引为3, 此处取了 2^16 个数
for (int i = 3 * 65536; i < 4 * 65536; i++) {rbm.add(i);
}
// 用来优化BitmapContainer, 优化为RunContainer
rbm.runOptimize();

2)分析

a)优化前

b)优化后

c)注释
                第一组由于数据最大为 2^16-1 所以最高位的索引为0,且个数没有超过4096,所以直接存到ArrayContainer中
                第二组数据的范围是 2^16 到 2^17-1 所以最高位索引为1,此时需要用BitmapContainer来存储低16位的数字
                第三组如果没有优化的话是BitmapContainer存储从 0 ~ 65535 的所有数据,如果优化以后则会用RunContainer存储,且只会存一个开始值0,还有一个步长65535,中间所有的值连续
需要注意的是第二组在调用优化方法以后并没有被优化成RunContainer
4、常用API

// and取交集
RoaringBitmap roaringBitmapAnd1 = RoaringBitmap.bitmapOf(1, 2, 3);
RoaringBitmap roaringBitmapAnd2 = RoaringBitmap.bitmapOf(3, 6, 4);
RoaringBitmap and = RoaringBitmap.and(roaringBitmapAnd1, roaringBitmapAnd2);
print(and, "and取交集后bitmap的值: ");
System.out.println("统计基数: " + and.getCardinality());
System.out.println("判断bitmap是否为空: " + and.isEmpty());
System.out.println("判断1与2是否相等: " + roaringBitmapAnd1.equals(roaringBitmapAnd2));
System.out.println("判断1与2是否相交: " + RoaringBitmap.intersects(roaringBitmapAnd1, roaringBitmapAnd2));
System.out.println("AND计算并返回基数: " + RoaringBitmap.andCardinality(roaringBitmapAnd1, roaringBitmapAnd2));
roaringBitmapAnd1.remove(2);// 这里是数值, 不是索引
print(roaringBitmapAnd1, "1中移除数字2: ");
//        roaringBitmapAnd2.flip(4L, 5L);// ??????
//        print(roaringBitmapAnd2, "2中翻转数字3: ");
System.out.println("bitmap1小于等于3的整数数目: " + roaringBitmapAnd1.rank(3));
System.out.println("bitmap2中是否包含10: " + roaringBitmapAnd2.contains(10));
//        System.out.println("bitmap1中是否有值在给出的范围: " + roaringBitmapAnd1.contains(2, 3));// ???
System.out.println("bitmap1中是否包含bitmapand: " + roaringBitmapAnd1.contains(and));
roaringBitmapAnd2.add(10);
print(roaringBitmapAnd2, "bitmap2中添加元素10: ");
System.out.println("bitmap1中添加元素6到8: " + RoaringBitmap.add(roaringBitmapAnd1, 6L, 11L));
System.out.println("添加完元素以后bitmap1的值: " + roaringBitmapAnd1);
System.out.println("======================");
​// or取并集
RoaringBitmap roaringBitmapOr1 = RoaringBitmap.bitmapOf(1, 2, 3);
RoaringBitmap roaringBitmapOr2 = RoaringBitmap.bitmapOf(3, 4, 5);
RoaringBitmap or = RoaringBitmap.or(roaringBitmapOr1, roaringBitmapOr2);
print(or, "or取并集后bitmap的值: ");
System.out.println("统计基数: " + or.getCardinality());
System.out.println("判断bitmap是否为空: " + or.isEmpty());
System.out.println("判断1与2是否相等: " + roaringBitmapOr1.equals(roaringBitmapOr2));
System.out.println("判断1与2是否相交: " + RoaringBitmap.intersects(roaringBitmapOr1, roaringBitmapOr2));
System.out.println("or计算并返回基数: " + RoaringBitmap.orCardinality(roaringBitmapOr1, roaringBitmapOr2));
System.out.println("======================");// xor取异或: 相同的都是0, 不同的为1
RoaringBitmap roaringBitmapXor1 = RoaringBitmap.bitmapOf(1, 2, 3);
RoaringBitmap roaringBitmapXor2 = RoaringBitmap.bitmapOf(3, 2, 5);
RoaringBitmap xor = RoaringBitmap.xor(roaringBitmapXor1, roaringBitmapXor2);
print(xor, "xor异或后取bitmap的值: ");
System.out.println("统计基数: " + xor.getCardinality());
System.out.println("判断bitmap是否为空: " + xor.isEmpty());
System.out.println("判断1与2是否相等: " + roaringBitmapXor1.equals(roaringBitmapXor2));
System.out.println("判断1与2是否相交: " + RoaringBitmap.intersects(roaringBitmapXor1, roaringBitmapXor2));
System.out.println("xor计算并返回基数: " + RoaringBitmap.xorCardinality(roaringBitmapXor1, roaringBitmapXor2));
System.out.println("======================");// andNot取差集
RoaringBitmap roaringBitmapAndNot1 = RoaringBitmap.bitmapOf(1, 2, 3);
RoaringBitmap roaringBitmapAndNot2 = RoaringBitmap.bitmapOf(3, 4, 5);
RoaringBitmap andNot1 = RoaringBitmap.andNot(roaringBitmapAndNot1, roaringBitmapAndNot2);
RoaringBitmap andNot2 = RoaringBitmap.andNot(roaringBitmapAndNot2, roaringBitmapAndNot1);
print(andNot1, "andNot计算bitmap1与bitmap2的差集: ");
System.out.println("统计基数: " + andNot1.getCardinality());
System.out.println("判断bitmap是否为空: " + andNot1.isEmpty());
System.out.println("判断1与2是否相等: " + roaringBitmapAndNot1.equals(roaringBitmapAndNot2));
System.out.println("判断1与2是否相交: " + RoaringBitmap.intersects(roaringBitmapAndNot1, roaringBitmapAndNot2));
System.out.println("andNot1计算并返回基数: " + RoaringBitmap.andNotCardinality(roaringBitmapAndNot1, roaringBitmapAndNot2));
System.out.println("---");
print(andNot2, "计算bitmap2与bitmap1的差集: ");
System.out.println("统计基数: " + andNot2.getCardinality());
System.out.println("判断bitmap是否为空: " + andNot2.isEmpty());
System.out.println("判断1与2是否相等: " + roaringBitmapAndNot2.equals(roaringBitmapAndNot1));
System.out.println("判断1与2是否相交: " + RoaringBitmap.intersects(roaringBitmapAndNot2, roaringBitmapAndNot1));
System.out.println("andNot2计算并返回基数: " + RoaringBitmap.andNotCardinality(roaringBitmapAndNot2, roaringBitmapAndNot1));
System.out.println("======================");private static void print(RoaringBitmap roaringBitmap, String message) {System.out.print(message);roaringBitmap.forEach((Consumer<? super Integer>)  i -> System.out.print(i + " "));System.out.println();
}## result
and取交集后bitmap的值: 3
统计基数: 1
判断bitmap是否为空: false
判断1与2是否相等: false
判断1与2是否相交: true
AND计算并返回基数: 1
1中移除数字2: 1 3
bitmap1小于等于3的整数数目: 2
bitmap2中是否包含10: false
bitmap1中是否包含bitmapand: true
bitmap2中添加元素10: 3 4 6 10
bitmap1中添加元素6到8: {1,3,6,7,8,9,10}
添加完元素以后bitmap1的值: {1,3}
======================
or取并集后bitmap的值: 1 2 3 4 5
统计基数: 5
判断bitmap是否为空: false
判断1与2是否相等: false
判断1与2是否相交: true
or计算并返回基数: 5
======================
xor异或后取bitmap的值: 1 5
统计基数: 2
判断bitmap是否为空: false
判断1与2是否相等: false
判断1与2是否相交: true
xor计算并返回基数: 2
======================
andNot计算bitmap1与bitmap2的差集: 1 2
统计基数: 2
判断bitmap是否为空: false
判断1与2是否相等: false
判断1与2是否相交: true
andNot1计算并返回基数: 2
---
计算bitmap2与bitmap1的差集: 4 5
统计基数: 2
判断bitmap是否为空: false
判断1与2是否相等: false
判断1与2是否相交: true
andNot2计算并返回基数: 2

三、压缩位图精确去重UDAF实现

1、构造表以及数据
   1)构造的表名称:mart_grocery_crm.bitmap_count_distinct_test_spark
   2)mart_grocery_crm.bitmap_count_distinct_test_spark 表中的数据(user_id为int类型)


   3)通过SQL查看分组去重后的结果

SELECT department, count(distinct user_id) FROM mart_grocery_crm.bitmap_count_distinct_test_spark GROUP BY department
--根据部门分组去重后的结果
————————————————————————————————————————————————
|  department  |  `count`(DISTINCT `user_id`)  |
|——————————————————————————————————————————————|
|    waimai    |              2                |
|——————————————————————————————————————————————|
|   xiaoxiang  |              3                |
|——————————————————————————————————————————————|
|    maicai    |              4                |
|——————————————————————————————————————————————|   

2、编写UDAF

1)继承 AbstractGenericUDAFResolver 抽象类,重写 getEvaluator 方法

/*** @author zhaocesheng* @since 2021/08/04* 通过RoaringBitmap实现CountDistinct测试类*/
public class RBMCountTestUDAF extends AbstractGenericUDAFResolver {
​/*** @param info UDAF方法入参* @return 该方法可以实现不同的入参走不同的实现类里面的实现逻辑* @throws SemanticException 可能会抛出语义异常错误*/@Overridepublic GenericUDAFEvaluator getEvaluator(TypeInfo[] info) throws SemanticException {// 校验长度if (info.length > 1) {throw new UDFArgumentTypeException(info.length - 1, "Exactly one argument is expected.");}// 校验类型switch (((PrimitiveTypeInfo) info[0]).getPrimitiveCategory()) {case INT:break;case BYTE:case SHORT:case LONG:case FLOAT:case DOUBLE:case TIMESTAMP:case DECIMAL:case STRING:case BOOLEAN:case DATE:default:throw new UDFArgumentTypeException(0,"Only numeric type arguments are accepted but " + info[0].getTypeName() + " was passed as parameter 1.");}// 只有一个实现逻辑就是Count Distinctreturn new RoaringBitmapCountEvaluator();}
getEvaluator 方法的目的感觉有两个:
一个是校验函数入参的个数以及入参的类型
一个是根据不同的入参判断走哪个子类,不同的子类对应着不同的实现逻辑(本例中的子类只有一个,也就是实现逻辑只有一个)

2)实现类以及init方法

public static class RoaringBitmapCountEvaluator extends GenericUDAFEvaluator {
​PrimitiveObjectInspector inputOI;
​@Overridepublic ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException {assert (parameters.length == 1);super.init(m, parameters);inputOI = (PrimitiveObjectInspector) parameters[0];// map和combine阶段返回RoaringBitmap的二进制数组if (m == Mode.PARTIAL1 || m == Mode.PARTIAL2) {return PrimitiveObjectInspectorFactory.javaByteArrayObjectInspector;}// 只有map和reduce的情况返回一个Count Distinct之后的数值return PrimitiveObjectInspectorFactory.javaIntObjectInspector;}
1  实现类需要继承GenericUDAFEvaluator
2  Hive的执行过程其实是mapreduce的过程,可以分为四种情况1)多个节点的map阶段收集数据 (对应着Mode.PARTIAL1)2)combine阶段部分聚合每个节点中map阶段的数据(对应着Mode.PARTIAL2)3)reduce阶段合并各个节点的数据(对应着Mode.FINAL)4)有些情况map阶段之后直接输出结果(对应着Mode.COMPLETE)
在本例中map阶段和combine阶段输出的结果为RoaringBitmap的字节数组byte[],只有map阶段以及reduce阶段返回最终结果(int类型)
3  PrimitiveObjectInspector是全局输入输出数据类型的OI实例,用于解析输入输出数据(后续的方法中会用到)

3)构建中间结果缓存Buffer

/*** 构建自己的缓冲Buffer*/
static class RoaringBitmapAgg implements AggregationBuffer {RoaringBitmap rbm;
​public byte[] serializeToByte() {ByteArrayOutputStream bos = new ByteArrayOutputStream();DataOutputStream dos = new DataOutputStream(bos);try {assert (rbm != null);rbm.serialize(dos);dos.close();} catch (IOException e) {e.printStackTrace();}return bos.toByteArray();}
​public RoaringBitmap deSerializeFromByte(byte[] bytes) {RoaringBitmap rbm = new RoaringBitmap();DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes));try {rbm.deserialize(dis);dis.close();} catch (IOException e) {e.printStackTrace();}return rbm;}
​
}
​
@Override
public AggregationBuffer getNewAggregationBuffer() throws HiveException {RoaringBitmapAgg rbmBuffer = new RoaringBitmapAgg();if (rbmBuffer.rbm == null) {rbmBuffer.rbm = new RoaringBitmap();} else {reset(rbmBuffer);}return rbmBuffer;
}
​
@Override
public void reset(AggregationBuffer agg) throws HiveException {RoaringBitmapAgg rbmBuffer = (RoaringBitmapAgg) agg;if (rbmBuffer != null && rbmBuffer.rbm != null) {rbmBuffer.rbm.clear();}
}1  RoaringBitmapAgg 类的目的是为了缓存 RoaringBitmap 聚集或者取并集后的部分结果
2  RoaringBitmapAgg 中的 serializeToByte 方法目的是为了将 RoaringBitmap 序列化成二进制流,在 map 或者 combine 阶段作为输出的结果使用
3  RoaringBitmapAgg 中的 deSerializeFromByte 方法目的是为了将二进制流反序列化成 RoaringBitmap,在 combine 或者 reduce 阶段将入参反序列化成 RoaringBitmap 后做合并计算时使用
4  getNewAggregationBuffer 方法在map阶段执行一次,目的是获取中间结果缓存对象
5  reset 方法mapreduce支持mapper和reducer的重用,所以为了兼容,也需要做内存的重用(不是很明白???)

4)map阶段的iterate方法

/*** map阶段各个节点将数据写入RoaringBitmap, 需要注意的是需要把参数转换成int类型** @param agg        buffer* @param parameters 列值* @throws HiveException UDF异常*/
@Override
public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException {assert (parameters.length == 1);RoaringBitmapAgg rbmBuffer = (RoaringBitmapAgg) agg;if (rbmBuffer != null && rbmBuffer.rbm != null && parameters[0] != null) {rbmBuffer.rbm.add(PrimitiveObjectInspectorUtils.getInt(parameters[0], inputOI));}
}
1  该方法只会发生在 map 阶段2  该方法的目的是为了聚集map阶段所在节点的有效数据3  此处需要通过 PrimitiveObjectInspector 以及入参得到int类型的列值,然后将列值写入 RoaringBitmap

5)terminatePartial方法

/*** map和combine阶段返回部分聚集结果** @param agg buffer* @return byte[]* @throws HiveException UDF异常*/
@Override
public Object terminatePartial(AggregationBuffer agg) throws HiveException {RoaringBitmapAgg rbmBuffer = (RoaringBitmapAgg) agg;if (rbmBuffer != null) {return rbmBuffer.serializeToByte();}return new Byte[0];
}
map 阶段或者 combine 阶段结束以后将结果序列化

6)merge方法

/*** combine和reducer阶段聚合数据** @param agg     buffer* @param partial 部分聚集数据* @throws HiveException UDF异常*/
@Override
public void merge(AggregationBuffer agg, Object partial) throws HiveException {RoaringBitmapAgg rbmBuffer = (RoaringBitmapAgg) agg;if (rbmBuffer != null && rbmBuffer.rbm != null) {rbmBuffer.rbm.or(rbmBuffer.deSerializeFromByte((byte[]) partial));}
}
combine 阶段或者 reduce 阶段将结果聚合在一起,本例中是将各个节点的 Roaringbitmap 取并集

7)terminate方法

/*** 得到reduce后的最终结果** @param agg buffer* @return 取并集后的最终结果* @throws HiveException UDF异常*/
@Override
public Object terminate(AggregationBuffer agg) throws HiveException {RoaringBitmapAgg rbmBuffer = (RoaringBitmapAgg) agg;if (rbmBuffer.rbm != null) {return rbmBuffer.rbm.getCardinality();}return -1;
}
输出 reduce 后的最终结果,该结果的类型与init方法中的定义的返回类型前后呼应

3、UDAF的工作流程总览
    1)方法执行流程

2)MR中数据流转流程

RoaringBitmap数据结构以及精确去重UDAF实现相关推荐

  1. Flink+Hologres亿级用户实时UV精确去重最佳实践

    简介:Flink+Hologres亿级用户实时UV精确去重最佳实践 UV.PV计算,因为业务需求不同,通常会分为两种场景: 离线计算场景:以T+1为主,计算历史数据 实时计算场景:实时计算日常新增的数 ...

  2. 精确去重和Roaring BitMap

    精确去重和Roaring BitMap 互联网行业常见的一个业务需求就是求UV(日活)和N日留存,这就涉及到去重计数(COUNT DISTINCT)的计算. BitMap概述 精确去重算法主要通过Bi ...

  3. ClickHouse基于全局字典与物化视图的精确去重方案

    clickhouse具有bitmap, 但只支持int, 实测表明groupBitmap()这个agg比直接的count(distinct x)计算要快至少一倍以上, 按之前druid中的测试 经验表 ...

  4. Spark多维分析去重计数场景优化案例【BitMap精确去重的应用与踩坑】

    关注交流微信公众号:小满锅 场景 前几天遇到一个任务,从前也没太注意过这个任务,但是经常破9点了,执行时长正常也就2个小时. 看逻辑并不复杂,基本是几段SQL的JOIN操作,其中一个最耗时间的就是要根 ...

  5. Redis 精确去重计数 —— 咆哮位图

    如果要统计一篇文章的阅读量,可以直接使用 Redis 的 incr 指令来完成.如果要求阅读量必须按用户去重,那就可以使用 set 来记录阅读了这篇文章的所有用户 id,获取 set 集合的长度就是去 ...

  6. redis统计用户日活量_Redis精确去重计数方法(咆哮位图)

    前言 如果要统计一篇文章的阅读量,可以直接使用 Redis 的 incr 指令来完成.如果要求阅读量必须按用户去重,那就可以使用 set 来记录阅读了这篇文章的所有用户 id,获取 set 集合的长度 ...

  7. 海量数据的非精确去重利器——从HyperLogLog到布谷鸟过滤器

    背景 非精确:牺牲一定准确度换取空间效率和时间效率. 统计网站的UV(独立访客数):当用户数量非常多时,比如几千万甚至上亿,那么使用普通的哈希表去重将会占用可怕的巨大内存空间.引用吴军博士的<数 ...

  8. 苏宁6亿会员是如何做到精确快速分析的?

    " 随着苏宁业务的高速发展,大数据平台对海量的业务数据分析越来越具有挑战,尤其是在精确去重.复杂 JOIN 场景下,如用户画像.UV.新老买家.留存.流失用户等. 图片来自 Pexels 随 ...

  9. 查询去重_如何在 1 秒内做到大数据精准去重?

    去重计数在企业日常分析中应用广泛,如用户留存.销售统计.广告营销等.海量数据下的去重计数十分消耗资源,动辄几分钟,甚至几小时,Apache Kylin 如何做到秒级的低延迟精确去重呢? 作者:史少锋, ...

  10. 千万数据去重_如何在 1 秒内做到大数据精准去重?

    去重计数在企业日常分析中应用广泛,如用户留存.销售统计.广告营销等.海量数据下的去重计数十分消耗资源,动辄几分钟,甚至几小时,Apache Kylin 如何做到秒级的低延迟精确去重呢? 什么是去重计数 ...

最新文章

  1. 为什么异步Python比同步Python快?
  2. JavaScript 中 window.setTimeout() 的详细用法
  3. java calendar类_2020 年,你还在使用 Java 中的 SimpleDateFormat 吗?
  4. JS双向数据绑定的原理介绍
  5. Tomcat启动报404(eclipse)
  6. Ulink2 No Ulink Device found 解决办法
  7. 在操作系统重启后恢复应用程序的工作状态
  8. rx java定时循环_Rxjava定时器异常循环
  9. android idle模式
  10. 大数据与云计算课后习题
  11. 程序员吐槽的“面试造火箭、工作拧螺丝”,用应聘司机的场景还原当下奇葩的面试
  12. 实例讲解反向传播(简单易懂)
  13. Bzoj 3654 图样图森波 题解
  14. TELEPORTSTONE.LUA --传送宝石
  15. APP如何在应用商店获取较高的排名?
  16. 【汇正财经】股票价格有哪些偏向性特征?
  17. 如何通过QQ邮箱获取授权码
  18. 【JS】网页悬浮广告及联系QQ客服侧边栏
  19. ardupilot/arduplane/attitude.cpp 姿态控制解析
  20. linux下故障硬盘点灯操作

热门文章

  1. MAML代码及理论的深度学习 PyTorch二阶导数计算
  2. excel之天数转年月日
  3. 三大指数快速入门和应用
  4. python理财基金数据分析可视化系统
  5. ABAP 关于 delete adjacent duplicates from的小心得
  6. 蒙特卡洛(Monte Carlo)方法的理解
  7. 我的JavaScript学习之路四:JavaScript数据类型之Number类型(1)
  8. Ceres Solver实现简单的光束法平差
  9. 云计算数据中心是什么,云计算数据中心和传统IDC有何区别?
  10. 什么是顶尖的互联网产品经理?