에서 일곱의 예를 만든 수요의 최종 종류에서 "통계 테이블의 총 가치의 총 수에 해당하는 모든 필드 드 - 중복, 그리고 비어 있지 않은 해당 필드 값을 요구하면서." 당신은 세븐의 예를 본 적이 있다면, 분명히 당신은 해결하는 방법을 알고 있어야합니다.
다음과 같이이 글을 쓰는 목적 :
그리고 오해를 피하기 위해, 세부 사업의 요구 사항을 설명
사업의 문제의 분석을 제공
다양한 솔루션의 예를 표시
2. 수요 쇼
다음과 같이 테이블에 앞서 tb_express, 예는 다음과 같습니다
이름
주소
무역
고치다
표현하다
왕 우
청두 (成都), 사천
d72_network
ty002
ZTO
없는
Yubei
z03_locker
bk213
SF-명시
리 레이
창사, 후난
없는
없는
SF-명시
......
......
......
......
......
신원 미상
광저우, 광동
t92_locker
tu87
테이블
표 설명
5 도시 여기 50 개 전체 테이블 필드의 합계 (이하, 편의상 표시, 예로서만이 다섯)
10 억 개 데이터의 전체 표 A의 총
데이터 소스의 문제 때문에, 필드 널 (null) 값 상황이 많이있을 것입니다. (최근 통계에 따르면 알아야 할 사항 수정 필드에게 9 억 5000 만 null이 아닌 값, 200-300000000의 범위에서 다른 분야에서 null이 아닌 값의 총 수의 총)
비즈니스 요구 :
모든 계산하기 위해 필요 분야의 총 가치 에 중복 제거 후 필드 값의 총 수 및 요청 필드가 비어 있지 않습니다
3. 분석
3.1 하나의 문제 (낮은 성능의 SQL)
사실, 사업 자체가 첫번째 생각 될 수있다 첫째는 SQL로 처리되어야하는 매우 간단한 요구입니다, 예는 다음과 같습니다 :
SELECTcount(name),count(distinct name)FROM tb_express
WHERE name ISNOTNULLAND name !='';
SQL은 한 번에 모든 필드를 카운트 할 수 있도록 각 필드의 널 (null)이 다르기 때문에 그러나, 50 개 필드는 다시 실행했다. 이 단점을 볼 수 있습니다 클러스터 자원 소비, 그것은 시간이 오래 걸립니다.
3.2 번째 문제 (데이터 스큐)
그래서, 당신은 문제를 해결하기 위해 코드를 작성하려고 할 수도 있습니다. 다음과 같이 일반 예는 준비 :
import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types.{StringType, StructField, StructType}
/**
* Description: '字段值总数'与'字段值去重后的总数'的统计(错误示例)
* <br/>
* Date: 2019/12/2 16:57
*
* @author ALion
*/
object CountDemo {
val expressSchema: StructType = StructType(Array(
StructField("name", StringType),
StructField("address", StringType),
StructField("trade", StringType),
StructField("fix", StringType),
StructField("express", StringType)
))
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("CountDemo")
val spark = SparkSession.builder()
.config(conf)
.getOrCreate()
val expressDF = spark.read.schema(expressSchema).table("tb_express")
val resultRDD = expressDF.rdd
// .flatMap { row =>
// val name = row.get(row.fieldIndex("name"))
// val address = row.get(row.fieldIndex("address"))
// val trade = row.get(row.fieldIndex("trade"))
// val fix = row.get(row.fieldIndex("fix"))
// val express = row.get(row.fieldIndex("express"))
//
// val buffer = ArrayBuffer[(String, String)]()
// // 去除null值
// // 字段名设置为key,字段值设置为value
// if (name != null) buffer.append(("name", name.toString))
// if (address != null) buffer.append(("address", address.toString))
// if (trade != null) buffer.append(("trade", trade.toString))
// if (fix != null) buffer.append(("fix", fix.toString))
// if (express != null) buffer.append(("express", express.toString))
//
// buffer
// }
.flatMap { row =>
// 更函数式的写法,也更简短
Array("name", "address", "trade", "fix", "express")
.flatMap { name =>
Option(row.get(row.fieldIndex(name))) match {
case Some(v) => Some((name, v.toString))
case None => None
}
}
}.groupByKey()
.mapValues { iter => (iter.size, iter.toSet.size) } // 此处计算'字段值总数'与'字段值去重后的总数'
// 拉取数据,打印结果
resultRDD.collect()
.foreach { case (fieldName, (count, distinctCount)) =>
println(s"字段名 = $fieldName, 字段值总数 = $count, 字段值去重后的总数 = $distinctCount")
}
spark.stop()
}
}
쓰기 코드는 일반적으로 첫 번째 생각은 이런 식으로, 각 그룹화 필드 이름이며, 필드 값은 통계이다.
그러나, 여기서 문제는 상기 우리에서 언급 된 코드 데이터 리드가 경사 GroupByKey에서이다 "수정 필드 합계 950 000 000 비 - 널 값 200-300 만 범위 내의 다른 필드의 비 - 널 값의 총 수." 필드가 핵심한다면, 그것은 GroupByKey에서이며, 셔플 노드 데이터로 이어질 것입니다 것은 다른 노드보다 훨씬 더 크다.
3.3 문제 트리스 (데이터 내의 데이터 비스듬히 경사)
당신은 아마, 원하는 것이 시간 "그것을가 기울어지지 않도록 내가 필드 이름 이외에 키를 넣어, 보증금 값을, 필드 값을 추가?" 몇 가지 예는 신체 있습니다 :
그러나 실행 그래서 한 번 위로, "이러한 비즈니스 요구 사항의 값을 배제 할 수는 없지만 수정 필드 억 5000 만 null이 아닌 값의 총 80 %가, 같은 값이다"고 숨겨진 비밀의 전면에서, 데이터는 것이다 여전히 틸트, 카드 위의 점. (하지만 당신은 분명히 reduceByKey 해결하여 생각해야)
또한, 메모리 소비를 줄이기 위해, 필드 이름 (String)를 나타내는 디지털 번호 (INT)를 사용할 수 있습니다. 그러나 이후 쉽게 볼 예, 또는 필드 이름을 사용.
해결책의 실시 예 4. 다양한
분명히 우리는 데이터에 문제가 거짓말 왜곡 것을 알고, 이전 수요 분석을 읽고, 필요가 그것을 해결하는 방법을 찾을 수 있습니다. 그런 다음 일반적인 솔루션은 reduceByKey이지만, 우리는 또한 여기에 내가 참조를 위해 몇 가지 예를 쓸 것, 다른 방법을 생각할 수있다.
4.1 난수의 사용은 데이터의 스큐 문제를 해결하는 방법으로 키를 첨가
val resultRDD = expressDF.rdd
.flatMap { row =>
val random = new Random()
// 更函数式的写法,也更简短
Array("name", "address", "trade", "fix", "express")
.flatMap { name =>
Option(row.get(row.fieldIndex(name))) match {
// 随机范围取100,最终会导致数据分成100份。根据当前集群启动的节点数合理取值,可以达到更好的效果。
case Some(v) => Some((name + "_" + random.nextInt(100), v.toString))
case None => None
}
}
}.groupByKey()
.map { case (k, v) =>
// 完成本次聚合,并去掉随机数
(k.split("_")(0), (v.size, v.toSet))
}.groupByKey() // 同样的,你也可以写reduceByKey,不过此处几乎没有效率影响
.mapValues {iter =>
// (字段值总数, 字段值去重后的总数)
(iter.map(_._1).sum, iter.map(_._2).reduce(_ ++ _).size)
}
4.2 사용 reduceByKey, 주요 데이터 구조를 수정 한 다음 이후의 처리 방법 변경
val resultRDD = expressDF.rdd
.flatMap { row =>
// 更函数式的写法,也更简短
Array("name", "address", "trade", "fix", "express")
.flatMap { name =>
Option(row.get(row.fieldIndex(name))) match {
case Some(v) => Some(((name, v.toString), 1))
case None => None
}
}
}.reduceByKey(_ + _) // 将问题分析最后示例中的groupByKey替换为reduceByKey即可解决
.map { case ((name, value), count) => (name, (value, count)) }
// 第二次groupByKey虽然还是以字段名为key,但是因为数据量很小,所以会很快处理完。
// 当然你这里也可以使用reduceByKey。
.groupByKey()
.map { case (name, iter) =>
// (字段名, 字段值总数, 字段值去重后的总数)
(name, iter.map(_._2).sum, iter.size)
}
4.3, 키 데이터 구조를 수정 애그리 게이터 (aggregator) 준비하지 않습니다
그리 게이터 클래스 CountAggregator
class CountAggregator(var count: Int, var countSet: mutable.HashSet[String]) {
def +=(element: (Int, String)) : CountAggregator = {
this.count += element._1
this.countSet += element._2
this
}
def ++=(that: CountAggregator): CountAggregator = {
this.count += that.count
this.countSet ++= that.countSet
this
}
}
object CountAggregator {
def apply(): CountAggregator =
new CountAggregator(0, mutable.HashSet[String]())
}