开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

【操作系统】进程调度策略

发表于 2021-10-17

公众号「码农小奎」 操作系统—进程调度算法

上一篇谈论了进程是什么,以及多进程系统中CPU需要在进程之间来回切换。

那么CPU到底如何进行上下文切换,假如进程A需要的运算时间长,而进程B虽然运算时间短,但用户急于获得进程B的响应。

这时候,进程切换如何兼顾效率与公平就显得很重要了。

其实如果只考虑最大化一个方面,那算法实现相对比较简单,但想兼顾两个方面难度就瞬间提升了。

在谈论算法之前,先来看看什么是调度吧。

调度

每个进程当然都希望自己能够被CPU快速执行,但是CPU就那一个,现在进程动辄上百。条件不允许进程独享CPU。

只能靠操作系统实现一系列调度策略,CPU要照顾好每一个进程。(CPU:我好难)

之前说过进程有三种基本状态,每一次状态的切换其实也意味着进程发生了切换,也就是操作系统进行了一次进程调度。

进程状态

调度发生的时机:

进程的基本状态在「运行-阻塞-就绪」之前来回变动,而每一次状态更迭会触发一次进程调度。

  • 就绪态->运行态:当进程创建完毕后就会进入就绪队列,等待操作系统选择队列中的进程执行。
  • 运行态->阻塞态:当进程发生IO事件时,进程进入阻塞队列,此时操作系统会从就绪队列中选择一个进程执行。
  • 运行态->结束态:当进程执行完毕退出后,操作系统会从就绪队列中选择一个进程执行。

当进程发生状态更迭的时候,操作系统会考虑是否把CPU执行权让位给其他进程,而选择什么时机让位,怎么让位,选择哪个进程上位,这就是所谓的进程调度策略。

进程调度策略有很多,根据如何处理时钟的周期性中断把进程调度算法分为两大类:

补充:所谓时钟中断,是指硬件上CPU的每秒数字脉冲的震荡次数,代表CPU的主频,每一次周期的到来,对应着一次CPU运算。也就是说,时钟频率越高,CPU单位时间内的运算量越大。硬件发烧友常说的“超频”来提升CPU性能,就是这个原理。

  • 非抢占式调度算法不理会时钟中断,只有在进程被阻塞或退出时,才会调度另一个进程。
  • 抢占式调度算法会让每个进程运行一个周期,而在周期结束时,如果该进程仍在运行,则会把它挂起,然后操作系统从就绪队列中选择一个进程执行。每一次周期的末端发生一次中断,CPU给每个进程执行一小会,称为时间片机制。

在谈论具体算法之前,还需要了解进程调度算法的衡量指标,这样才能理解算法为什么这么考虑。

  • CPU利用率:如果进程发生了IO请求,那此时CPU是空闲的,进程因等待硬盘返回数据而阻塞。频繁的IO请求势必会降低CPU利用率,所以这时候调度程序会从就绪队列中选择一个进程执行。
  • 系统吞吐率:是指CPU单位时间内完成的任务数量。如果一个进程的执行耗费时间很长,一直占用着CPU,那对后续的短作业进程是不公平的,造成进程的就绪队列堆积堵塞,
  • 周转时间:是指进程的运行时间加上进程的等待时间,假如进程只需1s执行完,却等待了5s,这种周转时间的过长是我们不希望发生的。
  • 等待时间:是指进程在就绪队列中的等待时长,如果一个进程一直等不到CPU的执行权,称为进程“饿死”。
  • 响应时间:交互式系统中考虑的权重比非常高的一个指标,是指用户点击鼠标或键盘后,到进程给出响应的时间,当然是越快越好。

明白了这些指标之后,就知道调度算法的具体优化方向了,接下来谈论具体的调度算法。

进程调度算法

进程调度算法有很多,它们不是同一时期出现的,而是根据需要不断发展而来的,下面根据历史发展的顺序讲述进程调度算法。

  1. 先来先服务调度算法

一种简单存粹的算法,先来先服务(First Come First Served, FCFS),顾名思义,就是传统的先来后来,调度程序从就绪队列里选择一个进程,直到该进程执行结束或被阻塞,才接着从就绪队列里选择一个进程。

听起来很公平,实际上当一个长作业先运行,那么后面的短作业等待时间就很长。就好像你排队买饭,明明队伍就三个人,结果第一个人帮5个舍友带饭,那对于后面排队的人来说等待时间太长了。

  1. 最短作业优先调度算法

最短作业优先(Shortest Job First, SJF),也很好理解,就是选择执行时间短的进程先运行。还是拿上面那个例子来说,相当于让帮忙带饭的那个人排到最后去,谁买的饭越少谁先来。

同样问题又来了,如果短作业很多,就绪队列里送来源源不断的短作业,那长作业按这种策略一直得不到CPU执行权,进程会“饿死”。
2. 高响应比优先算法

前面的先来先服务算法对长作业有利,最短作业优先算法对短作业有利。却都达不到对长作业和短作业调度的平衡。

高响应比主要按照优先权对进程调度,

优先权=(等待时间 + 要求服务时间) / 要求服务时间

由公式可以发现,两个进程如果服务时间一样,那么谁等待时间长谁优先。

而如果等待时间一致,则谁要求服务时间短谁优先。
3. 时间片轮转调度算法

公平且纯粹的时间片轮转调度(Round Robin,RR),给每个进程分配一个时间片,CPU轮流切换进程执行。

如果一个时间片结束,该进程还没执行完,则挂起,切换另一个进程执行。

或者在一个时间片结束前该进程已经结束或阻塞,则立即切换另一个进程执行。
4. 最高优先级调度算法

前面那个时间片轮转调度过于公平,实际上进程的任务的紧急程度是不一样的,有些进程可以在后台慢慢运算,而有些进程急于给用户响应。

所以在时间片轮转的基础上,加上了优先级,调度程既希望给每个进程轮转调度,又希望每次轮转尽可能选择高优先级的进程
5. 多级反馈队列调度算法

多级反馈队列(Multilevel Feedback Queue),关键词多级,反馈。

「多级」就是就绪队列不再是一个,而是多个,每个队列优先级不同,时间片也不同。

「反馈」意味着这个算法是动态的,根据当前进程的变化而变化。

看看这个机制是怎么工作的:

  • 有多个队列,每个队列优先级不同,队列优先级越高,时间片越短。
  • 新来的进程会先进入最高优先级队列,如果在一级队列的时间片内没有执行完,该进程会被向下送入二级队列,以此类推,直至进程执行完成。
  • 只有高级队列为空时,才执行低级队列中的进程。假如正在执行低级队列中的进程,此时高级队列中来了新进程,则停止运行当前进程,转而运行高级队列中的进程。

可以发现,这个算法兼顾了公平和效率。首先,短作业进入高级队列可以很快被执行完毕而不会进入低级队列,保证了短作业响应速度。其次,长作业可能在高级队列的时间片内执行不完,会被移入低级队列,虽然低级队列的优先权低,但时间片长,也就是说,低级队列被选中执行的概率相对小,但如果选中则执行的时间长。

END

进程状态的每一次切换都意味着发生了一次进程调度。

而进程调度的策略至关重要,调度算法指标总结来说就是公平与效率的取舍。

各种调度算法各有优势,在不同场景不同系统选择适当的调度策略。


欢迎关注「码农小奎」,更多技术想和你分享。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 17的这些新特性不看后悔

发表于 2021-10-17

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

点赞再看,养成习惯,评论可参与掘金周边抽奖。

前言

2021年9月14日Java 17发布,作为新时代的农民工,有必要了解一下都有哪些新东西。

Java 17是Java 11以来又一个LTS(长期支持)版本,Java 11 和Java 17之间发生了那些变化可以在OpenJDK官网找到JEP(Java增强建议)的完整列表。

本期文章会重点介绍在语法方面Java 17的更新,并且通过一些代码示例让大家更容易理解,主要涉及以下9个点:

  • 文本块
  • switch表达式
  • record关键字
  • sealed classes密封类
  • instanceof模式匹配
  • Helpful NullPointerExceptions
  • 日期周期格式化
  • 精简数字格式化支持
  • Stream.toList()简化

文本块

在Java17之前的版本里,如果我们需要定义一个字符串,比如一个JSON数据,基本都是如下方式定义:

1
2
3
4
5
6
7
8
java复制代码public void lowVersion() {
String text = "{\n" +
" \"name\": \"小黑说Java\",\n" +
" \"age\": 18,\n" +
" \"address\": \"北京市西城区\"\n" +
"}";
System.out.println(text);
}

这种方式定义具有几个问题:

  • 双引号需要进行转义;
  • 为了字符串的可读性需要通过+号连接;
  • 如果需要将JSON复制到代码中需要做大量的格式调整(当然这一点也可以通过其他工具解决);

通过Java 17中的文本块语法,类似的字符串处理则会方便很多;通过三个双引号可以定义一个文本块,并且结束的三个双引号不能和开始的在同一行。

上面例子中的JSON可以更方便,可读性更好的通过文本块定义。代码如下:

1
2
3
4
5
6
7
8
9
10
java复制代码private void highVersion() {
String text = """
{
"name": "小黑说Java",
"age": 18,
"address": "北京市西城区"
}
""";
System.out.println(text);
}

这段代码的输出结果是:

1
2
3
4
5
json复制代码{
"name": "小黑说Java",
"age": 18,
"address": "北京市西城区"
}

switch表达式

Java 17版本中switch表达式将允许switch有返回值,并且可以直接作为结果赋值给一个变量,等等一系列的变化。

下面有一个switch例子,依赖于给定的枚举值,执行case操作,故意省略break。

1
2
3
4
5
6
7
8
9
10
java复制代码private static void lowVesion(Fruit fruit) {
switch (fruit) {
case APPLE, PEAR:
System.out.println("普通水果");
case MANGO, AVOCADO:
System.out.println("进口水果");
default:
System.out.println("未知水果");
}
}

我们调用这个方法传入一个APPLE,会输出以下结果:

1
2
3
shell复制代码普通水果
进口水果
未知水果

显然这不是期望的结果,因为我们需要在每个case里添加break防止所有的case都没执行。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码private static void lowVesion(Fruit fruit) {
switch (fruit) {
case APPLE, PEAR:
System.out.println("普通水果");
break;
case MANGO, AVOCADO:
System.out.println("进口水果");
break;
default:
System.out.println("未知水果");
}
}

可以通过switch表达式来进行简化。将冒号(:)替换为箭头(->),并且switch表达式默认不会失败,所以不需要break。

1
2
3
4
5
6
7
java复制代码private static void withSwitchExpression(Fruit fruit) {
switch (fruit) {
case APPLE, PEAR -> System.out.println("普通水果");
case MANGO, AVOCADO -> System.out.println("进口水果");
default -> System.out.println("未知水果");
}
}

switch表达式也可以返回一个值,比如上面的例子我们可以让switch返回一个字符串来表示我们要打印的文本。需要注意在switch语句的最后要加一个分号。

1
2
3
4
5
6
7
8
java复制代码private static void withReturnValue(Fruit fruit) {
String text = switch (fruit) {
case APPLE, PEAR -> "普通水果";
case MANGO, AVOCADO -> "进口水果";
default -> "未知水果";
};
System.out.println(text);
}

也可以直接省略赋值动作直接打印。

1
2
3
4
5
6
7
java复制代码private static void withReturnValue(Fruit fruit) {
System.out.println(switch (fruit) {
case APPLE, PEAR -> "普通水果";
case MANGO, AVOCADO -> "进口水果";
default -> "未知水果";
});
}

如果你想在case里想做不止一件事,比如在返回之前先进行一些计算或者打印操作,可以通过大括号来作为case块,最后的返回值使用关键字yield进行返回。

1
2
3
4
5
6
7
8
9
10
11
java复制代码private static void withYield(Fruit fruit) {
String text = switch (fruit) {
case APPLE, PEAR -> {
System.out.println("给的水果是: " + fruit);
yield "普通水果";
}
case MANGO, AVOCADO -> "进口水果";
default -> "未知水果";
};
System.out.println(text);
}

这个输出结果是:

1
2
shell复制代码给的水果是: APPLE
普通水果

当然也可以直接使用yield返回结果。

1
2
3
4
5
6
7
8
9
10
java复制代码private static void oldStyleWithYield(Fruit fruit) {
System.out.println(switch (fruit) {
case APPLE, PEAR:
yield "普通水果";
case MANGO, AVOCADO:
yield "进口水果";
default:
yield "未知水果";
});
}

record关键字

record用于创建不可变的数据类。在这之前如果你需要创建一个存放数据的类,通常需要先创建一个Class,然后生成构造方法、getter、setter、hashCode、equals和toString等这些方法,或者使用Lombok来简化这些操作。

比如定义一个Person类:

1
2
3
4
5
6
7
8
9
10
java复制代码// 这里使用lombok减少代码
@Data
@AllArgsConstructor
public class Person {
private String name;

private int age;

private String address;
}

我们来通过Person类做一些测试,比如创建两个对象,对他们进行比较,打印这些操作。

1
2
3
4
5
6
7
java复制代码public static void testPerson() {
Person p1 = new Person("小黑说Java", 18, "北京市西城区");
Person p2 = new Person("小白说Java", 28, "北京市东城区");
System.out.println(p1);
System.out.println(p2);
System.out.println(p1.equals(p2));
}

假设有一些场景我们只需要对Person的name和age属性进行打印,在有record之后将会变得非常容易。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public static void testPerson() {
Person p1 = new Person("小黑说Java", 18, "北京市西城区");
Person p2 = new Person("小白说Java", 28, "北京市东城区");
// 使用record定义
record PersonRecord(String name,int age){}

PersonRecord p1Record = new PersonRecord(p1.getName(), p1.getAge());
PersonRecord p2Record = new PersonRecord(p2.getName(), p2.getAge());
System.out.println(p1Record);
System.out.println(p2Record);
}

record也可以单独定义作为一个文件定义,但是因为Record的使用非常紧凑,所以可以直接在需要使用的地方直接定义。

record同样也有构造方法,可以在构造方法中对数据进行一些验证操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public static void testPerson() {
Person p1 = new Person("小黑说Java", 18, "北京市西城区");
Person p2 = new Person(null, 28, "北京市东城区");
record PersonRecord(String name, int age) {
// 构造方法
PersonRecord {
System.out.println("name " + name + " age " + age);
if (name == null) {
throw new IllegalArgumentException("姓名不能为空");
}
}
}
PersonRecord p1Record = new PersonRecord(p1.getName(), p1.getAge());
PersonRecord p2Record = new PersonRecord(p2.getName(), p2.getAge());
}

密封类 sealed class

密封类可以让我们更好的控制哪些类可以对我定义的类进行扩展。密封类可能对于框架或中间件的开发者更有用。在这之前一个类要么是可以被extends的,要么是final的,只有这两种选项。

密封类可以控制有哪些类可以对超类进行继承,在Java 17之前如果我们需要控制哪些类可以继承,可以通过改变类的访问级别,比如去掉类的public,访问级别为默认。比如我们在com.heiz.java11包中定义了如下的三个类:

1
2
3
4
5
6
7
java复制代码package com.heiz.java11;
public abstract class Fruit {
}
public class Apple extends Fruit {
}
public class Pear extends Fruit {
}

那么我们可以在另一个包com.heiz123.java11中写如下的代码:

1
2
3
4
5
6
java复制代码private static void test() {
Apple apple = new Apple();
Pear pear = new Pear();
Fruit fruit = apple;
class Avocado extends Fruit {};
}

既可以定义Apple,Pear,也可以将apple实例赋值给Fruit,并且可以对Fruit进行继承。

如果我们不想让Fruit在com.heiz.java11包以外被扩展,在Java11版本中只能改变访问权限,去掉class的public修饰符。这样虽然可以控制被被继承,但是也会导致Fruit fruit = apple;也编译失败;在Java 17中通过密封类可以解决这个问题。

1
2
3
4
5
6
7
8
9
java复制代码package com.heiz.java17;

public abstract sealed class Fruit permits Apple,Pear {
}
public non-sealed class Apple extends Fruit {
}
public final class Pear extends Fruit {

}

在定义Fruit时通过关键字sealed声明为密封类,通过permits可以指定Apple,Pear类可以进行继承扩展。

子类需要指明它是final,non-sealed或sealed的。父类不能控制子类是否可以被继承。

1
2
3
4
5
6
7
8
java复制代码private static void test() {
Apple apple = new Apple();
Pear pear = new Pear();
// 可以将apple赋值给Fruit
Fruit fruit = apple;
// 只能继承Apple,不能继承Fruit
class Avocado extends Apple {};
}

instanceof模式匹配

通常我们使用instanceof时,一般发生在需要对一个变量的类型进行判断,如果符合指定的类型,则强制类型转换为一个新变量。

1
2
3
4
5
6
java复制代码private static void oldStyle(Object o) {
if (o instanceof Fruit) {
Fruit fruit = (GrapeClass) o;
System.out.println("This fruit is :" + fruit.getName);
}
}

在使用instanceof的模式匹配后,上面的代码可进行简写。

1
2
3
4
5
java复制代码private static void oldStyle(Object o) {
if (o instanceof Fruit fruit) {
System.out.println("This fruit is :" + fruit.getName);
}
}

可以将类型转换和变量声明都在if中处理。同时,可以直接在if中使用这个变量。

1
2
3
4
5
java复制代码private static void oldStyle(Object o) {
if (o instanceof Fruit fruit && fruit.getColor()==Color.RED) {
System.out.println("This fruit is :" + fruit.getName);
}
}

因为只有当instanceof的结果为true时,才会定义变量fruit,所以这里可以使用&&,但是改为||就会编译报错。

Helpful NullPointerExceptions

Helpful NullPointerExceptions可以在我们遇到NPE时节省一些分析时间。如下的代码会导致一个NPE。

1
2
3
4
java复制代码public static void main(String[] args) {
Person p = new Person();
String cityName = p.getAddress().getCity().getName();
}

在Java 11中,输出将显示NullPointerException发生的行号,但不知道哪个方法调用时产生的null,必须通过调试的方式找到。

1
2
java复制代码Exception in thread "main" java.lang.NullPointerException
        at com.heiz.java17.HelpfulNullPointerExceptionsDemo.main(HelpfulNullPointerExceptionsDemo.java:13)

在Java 17中,则会准确显示发生NPE的精确位置。

1
2
java复制代码Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.heiz.java17.Address.getCity()" because the return value of "com.heiz.java17.Person.getAddress()" is null
at com.heiz.java17.HelpfulNullPointerExceptionsDemo.main(HelpfulNullPointerExceptionsDemo.java:13)

日期周期格式化

在Java 17中添加了一个新的模式B,用于格式化DateTime,它根据Unicode标准指示一天时间段。

使用默认的英语语言环境,打印一天的几个时刻:

1
2
3
4
5
6
java复制代码DateTimeFormatter dtf = DateTimeFormatter.ofPattern("B");
System.out.println(dtf.format(LocalTime.of(8, 0)));
System.out.println(dtf.format(LocalTime.of(13, 0)));
System.out.println(dtf.format(LocalTime.of(20, 0)));
System.out.println(dtf.format(LocalTime.of(23, 0)));
System.out.println(dtf.format(LocalTime.of(0, 0)));

输出结果:

1
2
3
4
5
shell复制代码in the morning
in the afternoon
in the evening
at night
midnight

如果是中文语言环境,则输出结果为:

1
2
3
4
5
shell复制代码上午
下午
晚上
晚上
午夜

可见咱们的晚上是包括英美国家的evening和night的。

精简数字格式化支持

在NumberFormat中添加了一个工厂方法,可以根据Unicode标准以紧凑的、人类可读的形式格式化数字。

SHORT格式如下所示:

1
2
3
4
java复制代码NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));

输出格式为:

1
2
3
shell复制代码1K
100K
1M

LONG格式如下所示:

1
2
3
4
java复制代码fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));

输出结果为:

1
2
3
shell复制代码1 thousand
100 thousand
1 million

Stream.toList()

如果需要将Stream转换成List,需要通过调用collect方法使用Collectors.toList(),代码非常冗长。

1
2
3
4
5
6
7
java复制代码private static void oldStyle() {
Stream<String> stringStream = Stream.of("a", "b", "c");
List<String> stringList = stringStream.collect(Collectors.toList());
for(String s : stringList) {
System.out.println(s);
}
}

在Java 17中将会变得简单,可以直接调用toList()。

1
2
3
4
5
6
7
java复制代码private static void streamToList() {
Stream<String> stringStream = Stream.of("a", "b", "c");
List<String> stringList = stringStream.toList();
for(String s : stringList) {
System.out.println(s);
}
}

最后

本期内容带大家快速回顾了自上一个LTS版本Java 11以来添加的一些语法特性。除了这些语法上的更新之外,在Java 17中还有一些关于JVM、Libraries、打包工具等等一大堆功能,这里就不逐个跟大家介绍了,可以在OpenJDK官网进行查看。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Sentinel滑动时间窗限流算法

发表于 2021-10-17

Sentinel系列文章

Sentinel熔断限流器工作原理

Sentinel云原生K8S部署实战

Sentinel核心源码解析

时间窗限流算法

1
2
3
复制代码如图 
10-20这个时间窗内请求数量是60小于阈值100,这60个请求均可以通过
30-40这个时间窗请求数量是120大于阈值100,其中有20个请求不能通过

弊端

1
2
3
4
5
6
7
8
复制代码10t到16t 10个请求
16t-20t 50个请求
20t-26t 60个请求
26t到30t 20个请求

16t到26t 有了110个请求 超过了阈值 
但这种固定时间窗口算法就不会做限制
不能做到任意时间段内做限流

滑动时间窗口

1
2
3
4
复制代码从黄线位置往前推一个时间窗口
看该时间窗窗口内请求数量是否大于阈值
如果大于阈值 该点的请求被拦截
小于阈值 则允许访问

1
2
3
4
5
arduino复制代码这种情况有8个请求不能通过

滑动时间窗口实现了在任意时间段内请求数量都不能超过阈值

但它也带来了新的问题 "重复统计 浪费系统资源"

1
复制代码分析点1和分析点2对应的2个时间窗有重叠的统计的部分

滑动时间窗口算法改进

1
2
3
4
5
复制代码如图每个时间窗100t、分成4段 每段25t 每一段对应一个统计数组元素
比如100-125这一段对应 a0 这个数组元素 记录了100-125这单位时间窗内的请求数量

上图棕色线在130的位置 属于125t-150t 这个单位时间窗口 对应数组a1元素
如果请求属于125t-150t这一窗口 统计的请求量都会加到该窗口对应的a1中

判断180t这点的请求是否可以通过

1
2
3
4
5
复制代码计算175t-180t之间的请求量 该时间窗对应a3
则获取a0的统计值+a1的统计值+a2的统计值+ (175t到180t之间的请求量)
看是否超过了阈值100
如果超过则不能通过
如果没有超过则可以通过

滑动时间窗口源码解析

  • 对数据的统计
  • 对统计数据的使用

分析这个方法

ArrayMetric

LeapArray

WindowWrap样本窗口实例 范型T为MetricBucket

1
2
3
复制代码windowLengthInMs 样本窗口长度
windowStart 样本窗口的起始时间戳
value 当前样本窗口的统计数据 其类型为MetricBucket

MetricBucket

MetricEvent数据统计的维度

1
复制代码统计这个时间窗口内 通过请求量、拦截请求量、异常请求量、成功请求量等6个维度的数据 放在LongAddr数组中

获取当前时间点所在的样本窗口

计算当前时间所在的样本窗口id

获取当前时间窗的开始时间

1
2
3
4
5
6
7
8
9
10
11
ini复制代码1、首先计算27t位于哪个时间窗:27/10=2

下标是0 落在下标为2的位置

2、计算27t这点的请求统计量累计在哪个LeapArray元素中

2%4=2 即a2的位置

3、获取27t所在窗口的开始时间

27t-27%10=20t

替换掉老的时间窗口

1
复制代码对象本身没变

将当前窗口的请求数据添加到当前样本窗口的统计数据中

通过维度数据

对统计数据如何使用

流控快速失败

1
复制代码以前的加上现在的

获取之前统计好的数据

1
python复制代码遍历数据将pass维度的数据取出来 求和

1
sql复制代码将当前遍历的样本窗口统计数据记录到result中

过时判断

1
复制代码当前时间-窗口起始时间 比时间窗还大 那就是过时了

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python爬虫利器之Beautiful Soup入门详解,

发表于 2021-10-17

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

Code皮皮虾 一个沙雕而又有趣的憨憨少年,和大多数小伙伴们一样喜欢听歌、游戏,当然除此之外还有写作的兴趣,emm…,日子还很长,让我们一起加油努力叭🌈

如果觉得写得不错的话,球球一个关注哦😉


1、简介

Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.Beautiful Soup会帮你节省数小时甚至数天的工作时间.


2、解析库

灵活又方便的网页解析库,处理高效,支持多种解析器。
利用它不用编写正则表达式即可方便地实现网页信息的提取。

解析器 使用方法 优势 劣势
Python标准库 BeautifulSoup(markup, “html.parser”) Python的内置标准库、执行速度适中 、文档容错能力强 Python 2.7.3 or 3.2.2前的版本中文容错能力差
lxml HTML 解析器 BeautifulSoup(markup, “lxml”) 速度快、文档容错能力强 需要安装C语言库
lxml XML 解析器 BeautifulSoup(markup, “xml”) 速度快、唯一支持XML的解析器 需要安装C语言库
html5lib BeautifulSoup(markup, “html5lib”) 最好的容错性、以浏览器的方式解析文档、生成HTML5格式的文档 速度慢、不依赖外部扩展

3、讲解

3.1、Tag(标签选择器)

==选择元素==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码import requests

from bs4 import BeautifulSoup

html = '''
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
'''
#使用BeautifulSoup对网页代码进行解析
#我这里使用的是Python标准库——html.parser
soup = BeautifulSoup(html, "html.parser")

# 获取html代码中的titile标签
print(soup.title)

在这里插入图片描述

注意:这里默认只匹配第一个,如果文章中有多个相同的标签,而且想要获取之后的标签,可根据class值或者一些其他方法进行定位,之后我会一一道来。

==获取名称==

1
python复制代码print(soup.title.name)

在这里插入图片描述
==获取属性==

在这里插入图片描述
在这里插入图片描述
==获取内容==

在这里插入图片描述

在这里插入图片描述

==嵌套选择==
在这里插入图片描述
在这里插入图片描述

==子节点==

tag的 .contents 属性可以将tag的子节点以列表的方式输出
通过tag的 .children 生成器,可以对tag的子节点进行循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
python复制代码import requests

from bs4 import BeautifulSoup

html = '''
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title">
<b>The Dormouse's story</b>
</p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
'''
soup = BeautifulSoup(html, "html.parser")

print(soup.p.contents)
print("="*30)
for i in soup.p.children:
print(i)

在这里插入图片描述
==父节点==

通过 .parent 属性来获取某个元素的父节点

在这里插入图片描述

在这里插入图片描述

通过元素的 .parents 属性可以递归得到元素的所有父辈节点

在这里插入图片描述

在这里插入图片描述

==兄弟节点==
在这里插入图片描述
在这里插入图片描述


3.2、标准选择器(find、find_all)

3.2.1、find_all()

find_all( name , attrs , recursive , string , **kwargs )

find_all() 方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件

在这里插入图片描述
在这里插入图片描述

==keyword 参数==

如果一个指定名字的参数不是搜索内置的参数名,搜索时会把该参数当作指定名字tag的属性来搜索,如果包含一个名字为 id 的参数,Beautiful Soup会搜索每个tag的”id”属性.

在这里插入图片描述

在这里插入图片描述

==自定义参数查找:attrs==

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


3.2.2、find()

find( name , attrs , recursive , text , **kwargs )

find返回单个元素,find_all返回所有元素

在这里插入图片描述

在这里插入图片描述


3.3、Select选择器

==select==

匹配全部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python复制代码import requests

from bs4 import BeautifulSoup

html = '''
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title">
<b>The Dormouse's story</b>
</p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
'''
soup = BeautifulSoup(html, "html.parser")

print(soup.select("p b"))
print(soup.select("p a"))
print(soup.select("head title"))

在这里插入图片描述

==select_one==

select_one只选择满足条件的第一个元素

在这里插入图片描述

在这里插入图片描述


4、实战

本次实战以百度首页为例

在这里插入图片描述

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码import requests

from bs4 import BeautifulSoup

headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
}

url = "https://www.baidu.com"
response = requests.get(url=url,headers=headers)

soup = BeautifulSoup(response.text,"html.parser")

#获取全部class为mnav c-font-normal c-color-t的标签,进行遍历
divs = soup.find_all(class_="mnav c-font-normal c-color-t")
for div in divs:
print(div)
print("="*40)

可见获取成功
在这里插入图片描述

接下来获取每个模块对应的URL和文本值

1
2
3
python复制代码for div in divs:
print(div['href'])
print(div.text)

在这里插入图片描述

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码import requests

from bs4 import BeautifulSoup

headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
}

url = "https://www.baidu.com"
response = requests.get(url=url,headers=headers)

soup = BeautifulSoup(response.text,"html.parser")

#第一种方法
#通过contents,获取子节点信息
a_data = soup.find(class_="hot-title").contents
print(a_data[0].text)

#第二种方法
#先通过find使用class值定位,在使用find找到其下的div标签也就是我们需要的
a_data2 = soup.find(class_="hot-title").find("div")
print(a_data2.text)

在这里插入图片描述



💖最后

我是 Code皮皮虾,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!

创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以一键三连哦!,感谢支持,我们下次再见~


一键三连.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Java字符串】字符串虽简单,但这些你不一定知道 前言:

发表于 2021-10-17

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

前言:

字符串是程序开发当中,使用最频繁的类型之一,有着与基础类型相同的地位(字符串不属于基本类型),甚至在 JVM(Java 虚拟机)编译的时候会对字符串做特殊的处理,比如拼加操作可能会被 JVM 直接合成为一个最终的字符串,从而到达高效运行的目的。

1 :构造方法:

将字节数组或者字符数组转成字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码String s1 = new String();//创建了一个空内容的字符串。

String s2 = null;//s2没有任何对象指向,是一个null常量值。

String s3 = "";//s3指向一个具体的字符串对象,只不过这个字符串中没有内容。

//一般在定义字符串时,不用new。

String s4 = new String("abc");

String s5 = "abc"; 一般用此写法

new String(char[]);//将字符数组转成字符串。

new String(char[],offset,count);//将字符数组中的一部分转成字符串。

2 :一般方法:

按照面向对象的思想:

2.1 获取:

2.1.1:获取字符串的长度。**length()** ;


2.1.2:指定位置的字符。char **charAt**(int index);


2.1.3:获取指定字符的位置。如果不存在返回-1,所以可以通过返回值-1来判断某一个字符不存在的情况。           
1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码 int indexOf(int ch);//返回第一次找到的字符角标

 int indexOf(int ch,int fromIndex); //返回从指定位置开始第一次找到的角标

 int indexOf(String str); //返回第一次找到的字符串角标

int indexOf(String str,int fromIndex);

 int lastIndexOf(int ch);

 int lastIndexOf(int ch,int fromIndex);

 int lastIndexOf(String str);
int lastIndexOf(String str,int fromIndex);

2.1.4:获取子串。
1
2
3
sql复制代码String substring(int start);//从start位开始,到length()-1为止.
String substring(int start,int end);//从start开始到end为止。//包含start位,不包含end位。
substring(0,str.length());//获取整串

2.2 判断:

2.2.1:字符串中包含指定的字符串吗?


        boolean **contains**(String substring);


2.2.2:字符串是否以指定字符串开头啊?


        boolean **startsWith**(string);


2.2.3:字符串是否以指定字符串结尾啊?


        boolean **endsWith(** string);


2.2.4:判断字符串是否相同


        boolean **equals**(string);//覆盖了Object中的方法,判断字符串内容是否相同。


2.2.5:判断字符串内容是否相同,忽略大小写。


        boolean **equalsIgnoreCase**(string) ;

2.3 转换:

2.3.1:通过构造函数可以将字符数组或者字节数组转成字符串。


2.3.2:可以通过字符串中的静态方法,将字符数组转成字符串。
1
2
3
4
5
6
7
arduino复制代码            static String copyValueOf(char[] );

            static String copyValueOf(char[],int offset,int count);

            static String valueOf(char[]);

            static String valueOf(char[],int offset,int count);

2.3.3:将基本数据类型或者对象转成字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码            static String valueOf(char);

            static String valueOf(boolean);

            static String valueOf(double);

            static String valueOf(float);

            static String valueOf(int);

            static String valueOf(long);

            static String valueOf(Object);

2.3.4:将字符串转成大小写。


        String **toLowerCase**();


        String **toUpperCase**();


2.3.5:将字符串转成数组。


        char[] **toCharArray**();//转成字符数组。


        byte[] **getBytes**();//可以加入编码表。转成字节数组。


2.3.6:将字符串转成字符串数组。切割方法。


        String[] **split**(分割的规则-字符串);


2.3.7:将字符串进行内容替换。**注意:修改后变成新字符串,并不是将原字符串直接修改。**


        String **replace**(oldChar,newChar);


        String replace(oldstring,newstring);


2.3.8: String **concat**(string); //对字符串进行追加。


        String **trim**();//去除字符串两端的空格


int **compareTo**();//如果参数字符串等于此字符串,则返回值 0;如果此字符串按字典顺序小于字符串参数,则返回一个小于 0 的值;如果此字符串按字典顺序大于字符串参数,则返回一个大于 0 的值。

3.StringBuffer 字符串缓冲区:

构造一个其中不带字符的字符串缓冲区,初始容量为 16 个字符。

特点:

1 :可以对字符串内容进行修改。

2 :是一个容器。

3 :是可变长度的。

4 :缓冲区中可以存储任意类型的数据。

5 :最终需要变成字符串。

容器通常具备一些固定的方法:

1 ,添加。

StringBuffer **append**(data):在缓冲区中追加数据。追加到尾部。


StringBuffer **insert**(index,data):在指定位置插入数据。

2 ,删除。

StringBuffer **delete**(start,end);删除从start至end-1范围的元素


StringBuffer deleteCharAt(index);删除指定位置的元素

//sb.delete(0,sb.length());//清空缓冲区。

3 ,修改。

 StringBuffer **replace**(start,end,string);将start至end-1替换成string


void **setCharAt**(index,char);替换指定位置的字符


void **setLength**(len);将原字符串置为指定长度的字符串

4 ,查找。 (查不到返回-1)

1
2
3
4
5
6
7
c复制代码    int indexOf(string); 返回指定子字符串在此字符串中第一次出现处的索引。

    int indexOf(string,int fromIndex);从指定位置开始查找字符串

    int lastIndexOf(string); 返回指定子字符串在此字符串中最右边出现处的索引。

    int lastIndexOf(string,int fromIndex); 从指定的索引开始反向搜索

5,获取子串。

string **substring**(start); 返回start到结尾的子串


string substring(start,end); 返回start至end-1的子串

6 ,反转。

StringBuffer **reverse**();字符串反转

4. StringBuilder 字符串缓冲区:

JDK1.5 出现StringBuiler; 构造一个其中不带字符的字符串生成器,初始容量为 16 个字符。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。

方法和StringBuffer一样;

5.StringBuffer 和 StringBuilder 的区别:

StringBuffer 线程安全。

StringBuilder 线程不安全。

单线程操作,使用StringBuilder 效率高。

多线程操作,使用StringBuffer 安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码        StringBuilder sb = new StringBuilder("abcdefg");

        sb.append("ak");  //abcdefgak

        sb.insert(1,"et");//aetbcdefg

        sb.deleteCharAt(2);//abdefg

        sb.delete(2,4);//abefg

        sb.setLength(4);//abcd

        sb.setCharAt(0,'k');//kbcdefg

        sb.replace(0,2,"hhhh");//hhhhcdefg
//想要使用缓冲区,先要建立对象。

        StringBuffer sb = new StringBuffer();     

        sb.append(12).append("haha");//方法调用链。

        String s = "abc"+4+'q';

        s = new StringBuffer().append("abc").append(4).append('q').toString();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
typescript复制代码class  Test{

    public static void main(String[] args) {

        String s1 = "java";

        String s2 = "hello";

        method_1(s1,s2);

        System.out.println(s1+"...."+s2); //java....hello

       

        StringBuilder s11 = new StringBuilder("java");

        StringBuilder s22 = new StringBuilder("hello");

        method_2(s11,s22);

        System.out.println(s11+"-----"+s22); //javahello-----hello

    }

    public static void method_1(String s1,String s2){

        s1.replace('a','k');

        s1 = s2;

    }

    public static void method_2(StringBuilder s1,StringBuilder s2){

        s1.append(s2);

        s1 = s2;

    }

}

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【C++STL容器篇之set】 set

发表于 2021-10-16

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

set

简介:所有元素都会在插入时自动排序

本质:set 与 multiset 属于关联式容器,底层结构是用二叉树实现的

set 构造器与赋值

【函数原型】:

1
2
3
4
5
c++复制代码- set<T> st; //默认构造函数

- set(const set &st); //拷贝构造函数

- set operator=(const set &st); //重载等号操作符

【demo】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
c++复制代码#include <iostream>
#include <set>

using namespace std;

void printSet(const set<int> &st)
{
if (st.empty())
{
cout << "set is empty ." << endl;
}
else
{
for (set<int>::const_iterator it = st.begin(); it != st.end(); it++)
{
cout << *it << " ";
}
cout << " , size : " << st.size() << endl;
}
}
void test01()
{
set<int> st;

st.insert(50);
st.insert(30);
st.insert(10);
st.insert(20);
st.insert(40);
printSet(st);

// 拷贝构造
set<int> st2(st);
printSet(st2);

// 赋值
set<int> st3;
st3 = st2;
printSet(st3);
}
int main()
{
test01();
return 0;
}

小结:

  • set 容器插入元素时用insert
  • set容器插入元素时会自动排序

set 大小和交换

【函数原型】:

1
2
3
4
5
c++复制代码- size(); //返回容器中元素的数目

- empty(); // 判断容器是否为空

- swap(); // 交换两个集合容器

【demo】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
c++复制代码#include <iostream>
#include <set>

using namespace std;

void printSet(const set<int> &st)
{
if (st.empty())
{
cout << "set is empty ." << endl;
}
else
{
for (set<int>::const_iterator it = st.begin(); it != st.end(); it++)
{
cout << *it << " ";
}
cout << " , size : " << st.size() << endl;
}
}
void test01()
{
set<int> st;

st.insert(50);
st.insert(30);
st.insert(10);
st.insert(20);
st.insert(40);

set<int> st2;
st2.insert(100);
st2.insert(300);
st2.insert(400);
st2.insert(200);
st2.insert(500);
cout << "===== before swap ===== " << endl;
cout << "set1 : ";
printSet(st);
cout << "set2 : ";
printSet(st2);

st.swap(st2);
cout << "===== after swap ===== " << endl;
cout << "set1 : ";
printSet(st);
cout << "set2 : ";
printSet(st2);
}
int main()
{
test01();
return 0;
}

set 插入元素和删除元素

【函数原型】:

1
2
3
4
5
6
7
8
9
c++复制代码- insert(elem); // 向容器中插入元素

- clear(); // 清除所有的元素

- erase(pos); // 删除pos迭代器所指的元素,返回下一个元素的迭代器

- erase(begin,end); // 删除[begin,end]区间中的元素,返回下一个元素的迭代器

- erase(elem); // 删除容器中值为elem的元素

【demo】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
c++复制代码#include <iostream>
#include <set>

using namespace std;

void printSet(const set<int> &st)
{
if (st.empty())
{
cout << "set is empty ." << endl;
}
else
{
for (set<int>::const_iterator it = st.begin(); it != st.end(); it++)
{
cout << *it << " ";
}
cout << " , size : " << st.size() << endl;
}
}
void test01()
{
set<int> st;

// 插入元素
st.insert(50);
st.insert(30);
st.insert(10);
st.insert(20);
st.insert(40);
printSet(st);

// 删除元素
st.erase(st.begin());
printSet(st);

// 清空元素
st.clear();
printSet(st);
}
int main()
{
test01();
return 0;
}

set 查找和统计

【函数原型】:

1
2
3
c++复制代码- find(key); // 查找key是否存在,若存在,则返回key对应的迭代器,若不存在,返回set.end();

- count(key); // 统计key的元素个数

【demo】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
c++复制代码#include <iostream>
#include <set>

using namespace std;

void printSet(const set<int> &st)
{
if (st.empty())
{
cout << "set is empty ." << endl;
}
else
{
for (set<int>::const_iterator it = st.begin(); it != st.end(); it++)
{
cout << *it << " ";
}
cout << " , size : " << st.size() << endl;
}
}
void test01()
{
set<int> st;

// 插入元素
st.insert(50);
st.insert(30);
st.insert(10);
st.insert(20);
st.insert(40);
printSet(st);

// 查找元素
set<int>::iterator pos = st.find(30);
if (pos != st.end())
{

cout << "find " << *pos << endl;
}
else
{
cout << "not found . " << endl;
}

// 统计元素个数
int num = st.count(10);
cout << "num = " << num << endl;
}
int main()
{
test01();
return 0;
}

set 与 multiset区别

  • set 不可以插入重复数据,而 multiset 可以
  • set 插入数据的同时会返回插入结果,表示插入是否成功
  • multiset 不会检测数据,可以插入重复数据
    【demo】:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
c++复制代码#include <iostream>
#include <set>

using namespace std;

// set和multiset的区别
void test01()
{
set<int> s;
pair<set<int>::iterator, bool> ret = s.insert(10);
if (ret.second)
{
cout << "set 第一次插入成功" << endl;
}
else
{
cout << "set 第一次插入失败" << endl;
}

ret = s.insert(10);
if (ret.second)
{
cout << "set 第二次插入成功" << endl;
}
else
{
cout << "set 第二次插入失败" << endl;
}

multiset<int> ms;
ms.insert(10);
ms.insert(10);
cout << "multiset:";
for (multiset<int>::iterator it = ms.begin(); it != ms.end(); it++)
{
cout << *it << " ";
}
cout << endl;
}
int main()
{
test01();
return 0;
}

pair对组创建

1
复制代码成对出现的数据,利用对组可以返回两个数据

【函数原型】:

1
2
3
c++复制代码- pair(type,type) p (value1,value2);

- pair(type,type) p = make_pair(value1,value2);

【demo】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c++复制代码#include <iostream>
#include <string>

using namespace std;

// 对组创建
void test01()
{
pair<string, int> p(string("张三"), 20);
cout << "姓名:" << p.first << ",年龄:" << p.second << endl;

pair<string, int> p2 = make_pair("李四", 22);
cout << "姓名:" << p2.first << ",年龄:" << p2.second << endl;
}
int main()
{
test01();
return 0;
}

set 容器排序

1
arduino复制代码set 容器默认排序规则为从小到大,利用仿函数,可以改变排序规则

【demo】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
c++复制代码#include <iostream>
#include <set>

using namespace std;

class MyCompare
{
public:
bool operator()(int v1, int v2)
{
return v1 > v2;
}
};

void test01()
{
set<int> s1;
s1.insert(19);
s1.insert(5);
s1.insert(10);
s1.insert(3);

cout << "默认从小到大排序:";
for (set<int>::iterator it = s1.begin(); it != s1.end(); it++)
{
cout << *it << " ";
}
cout << endl;

set<int, MyCompare> s2;
s2.insert(19);
s2.insert(5);
s2.insert(10);
s2.insert(3);
cout << "指定从大大小排序:";

for (set<int, MyCompare>::iterator it = s2.begin(); it != s2.end(); it++)
{
cout << *it << " ";
}
cout << endl;
}
int main()
{
test01();
return 0;
}

set 存放自定义数据类型

1
arduino复制代码对于自定义数据类型,set必须指定排序规则才可以插入数据

【demo】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
c++复制代码#include <iostream>
#include <string>
#include <set>

using namespace std;

class Person
{
public:
Person(string name, int age)
{
this->m_name = name;
this->m_age = age;
}

public:
string m_name;
int m_age;
};

class PersonCompare
{
public:
bool operator()(const Person &p1, const Person &p2)
{
// 按照年龄降序排列
return p1.m_age > p2.m_age;
}
};
void test01()
{
Person p1("张三", 20);
Person p2("李四", 23);
Person p3("王五", 22);
Person p4("赵六", 21);

set<Person, PersonCompare> s;
s.insert(p1);
s.insert(p2);
s.insert(p3);
s.insert(p4);

for (set<Person, PersonCompare>::iterator it = s.begin(); it != s.end(); it++)
{
cout << it->m_name << "," << it->m_age << endl;
}
}
int main()
{
test01();
return 0;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

解决 GitHub 访问慢的问题

发表于 2021-10-16

经常在github上找一些好玩的学习项目,但是经常出现github页面打开速度很慢甚至无法打开的情况,往往导致计划被打乱,瞬间不想学习了。今天就来总结一下解决github访问慢的方法。

GitHub访问慢或者无法访问一般是由以下问题引起的:

  1. 本地网络访问慢,科学上网速度很快
  2. 本地网络无法访问(响应时间过长导致无法访问)
  3. 由于github的加速分发CDN域名assets-cdn.github.com遭到DNS污染,无法访问

1. 修改本地hosts映射

域名解析直接指向GitHub的IP地址,以此来绕过DNS解析

1.1 查看最新GitHub 的最新ip地址

在 ip地址查询 网站中查询GitHub相关的网站对应的最新IP地址

  • github.com
  • github.global.ssl.fastly.net
  • assets-cdn.github.com
  • codeload.github.com

在当前网站中查询指定网站ip地址还可以使用另外方法:

①直接将网站作为参数进行请求,省略点击查询的步骤:

  • websites.ipaddress.com/github.glob…
  • websites.ipaddress.com/github.com
  • websites.ipaddress.com/assets-cdn.…
  • websites.ipaddress.com/codeload.gi…

②将ip查询网站拼接在之后进行查询

  • github.global.ssl.fastly.net.ipaddress.com/
  • github.com.ipaddress.com/
  • assets-cdn.github.com.ipaddress.com/
  • codeload.github.com.ipaddress.com/

1.2 本地hosts文件映射ip地址

找到对应的IP地址后,将IP地址与网站地址进行对应,并将对应关系写入本地hosts文件中。

在windows系统中的c:/Windows/System32/drivers/etc 下找到hosts文件,编辑打开,将四个网站的IP地址和网站地址对应写入进入,作为DNS的映射。

hosts文件直接编辑修改时可能没有权限,可以通过以下方法完成修改:

  1. 修改当前文件权限,右键hosts文件 -> 属性 -> 安全 -> 编辑 -> Users -> Users的权限后加入写权限
  2. 将当前文件复制到别的盘中,修改文件后复制回来覆盖原来文件
1
2
3
4
5
less复制代码#github dns映射 格式如:  [ip]: [domainName]
199.232.69.194 github.global.ssl.Fastly.net
140.82.114.4 GitHub.com
185.199.108.153 assets-cdn.Github.com
140.82.114.9 codeload.Github.com

1.3 刷新DNS缓存来访问新的映射

hosts文件内容更新成功后,还需要刷新windows系统的DNS才可以生效。

使用 win+R ,打开cmd命令行,输入 ipconfig/flushdns 刷新DNS缓存即可。

image-20211016150711634

刷新完成后,再次打开github网站时速度会明显提升,需要注意的是以上github网站的ip经常发生变化,如果访问再次变慢可以重新更新映射信息。

2. 一键更新

手动更新本地hosts文件的方式比较繁琐,我们可以编写程序来代替手动操作,实现需要时hosts文件内容的一键更新。

2.1 利用开源项目

推荐一个 github 开源项目:更新hosts ,作者会每日提供最新的相关 ip 地址映射信息,我们可以直接复制使用或者使用其中的程序进行一键更新操作。

作为一个coder,我们也可以自己去实现一个脚本程序。

3. Chrome插件

chrome 插件如 github 加速 等,可以实现在访问和下载项目时使用镜像加速,提升访问速度。

image-20211016153443393

如果无法访问chrome下载插件,推荐一个好用的chrome插件下载网站:CrxDL.COM ,下载后使用开发者模式安装插件即可使用。

更多 chrome 插件安装使用方法参考文档:chrome 插件食用指南!

4. 镜像网站

github访问速度慢,我们还可以使用镜像网站来代替github

  • hub.njuu.cf/search
  • www.gitclone.com/gogs/search…

以上网站作为镜像网站,基本包含了github上已有的项目信息,并且可以查看和下载相关项目,速度还不错,可以作为备用网站使用。

5. 离线下载(Gitee)

5.1 搜索下载

Gitee 是一个类似 Github 的国内代码托管平台,提供了大量国内开发者开源的项目,国内访问正常且速度很快,基本上比较有名的 GitHub 项目都可以在 Gitee 平台搜索到。

5.2 离线下载

对于使用 Gitee 平台无法直接搜索到的项目,可以借助 Gitee 平台项目管理实现 Github 项目的离线下载。

使用流程如下:

  1. 在 Gitee 中登录自己账户
  2. 在仓库页面,点击右上角 + 新增一个仓库,选择最后一个从 Github 导入

image.png

  1. 在导入页面设置需要导入的仓库地址,点击确定,等待导入完成。

image.png

  1. 等待一会后,Gitee 就会下载好对应的项目到我们自己的项目仓库中,之后便可以使用自己项目地址下载到本地使用。

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java往oracle存clob类型的值时,字符长度过长怎么

发表于 2021-10-16

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

业务场景

将照片转为数字长串后,由于字符过长,java往数据库中直接存为clob字段时,oracle会报ORA-01704问题:字符串文字过长。

这是因为一般对含有CLOB字段的数据操作。

如果CLOB字段的内容非常大的时候,会导致SQL语句过长。

隐式转换:oracle默认把字符串转换成varchar2类型,varchar2类型最大字符串的长度为4000,当字段长度比4000大时,所以会报ora-01704错误。

简言之,就是两个单引号之间的字符长度不能超过4000。

解决办法总结

试过几种办法,发现只有最后一种是有效的,现将错误与正确的方法都进行了总结,错误的进行避免,正确的可以借鉴。

1.将字符串按照固定长度截取,insert语句:to_clob(‘字符1’ || ‘字符2’ || ‘字符3’ || ‘字符4’ …….),此方法无效,隐式转换成了varchar2,字符长度超过4000;

2.将超长字段按照一定的长度进行截取,用to_clob()函数拼接insert语句,将截取的字符用连接符 ‘||’ 连接起来存入clob字段中,insert语句:to_clob(‘字符1’) || to_clob(‘字符2’) || to_clob(‘字符3’) || to_clob(‘字符4’) ……. 此方法有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码/**
* 将超长的内容转为clob可以保存的句式
* 字符串每隔2000长度插入指定字符串 ' ) || TO_CLOB( '
* @param original 处理超长字符串
* @param insertString 插入字符串 ') || TO_CLOB('
* @param interval 间隔的字符长度 2000
* @return
*/
public static String stringInsertByInterval(String original, String insertString, int interval) {
if (original == null) return "";
Integer len = original.length();
if (interval >= len) return original;

String rtnString = original;
if (original.length() > interval) {
List<String> strList = new ArrayList<String>();
Pattern p = Pattern.compile("(.{" + interval + "}|.*)");
Matcher m = p.matcher(original);
while (m.find()) {
strList.add(m.group());
}
strList = strList.subList(0, strList.size() - 1);
rtnString = StringUtils.join(strList, insertString);
}
rtnString = “'TO_CLOB('” + rtnString + "')";
return rtnString;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

关于SneakyThrows使用 1 异常引入 2 Sn

发表于 2021-10-16

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

关于@SneakyThrows注解的使用,主要是消除代码中异常处理代码.

1 异常引入

Java中异常Throwable分为两类, 一种是Exception类,称为受检异常(Checked Exception), 第二种是RuntimeException类, 运行时异常.

Exception类异常,强制要求方法抛出可能出现的异常,调用者必须处理这个异常. 一般在代码中,程序员通过捕获异常,再包一层RuntimeException,向外抛出.(常见如Spring源码中)

2 @SneakyThrows

1 SneakyThrows注解的源码

1
2
3
4
5
6
7
8
9
java复制代码@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.SOURCE)
public @interface SneakyThrows {
/** @return The exception type(s) you want to sneakily throw onward. */
Class<? extends Throwable>[] value() default java.lang.Throwable.class;

//The fully qualified name is used for java.lang.Throwable in the parameter only. This works around a bug in javac:
// presence of an annotation processor throws off the type resolver for some reason.
}

2使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class SneakyThrowDemo {

// 方法主动声明可能出现的异常
public void test1() throws IllegalAccessException, InstantiationException {
SneakyThrowDemo sneakyThrowDemo = SneakyThrowDemo.class.newInstance();
}

// 使用SneakyThrows注解
@SneakyThrows
public void test2() {
SneakyThrowDemo sneakyThrowDemo = SneakyThrowDemo.class.newInstance();
}

// test2方法的编译结果
public void test3() {
try {
SneakyThrowDemo sneakyThrowDemo = SneakyThrowDemo.class.newInstance();
} catch (Throwable e) {
// 调用Lombok方法转化为RuntimeException
throw Lombok.sneakyThrow(e);
}
}

}

其中Lombok.sneakyThrow()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码	/**
* Throws any throwable 'sneakily' - you don't need to catch it, nor declare that you throw it onwards.
* The exception is still thrown - javac will just stop whining about it.
* <p>
* Example usage:
* <pre>public void run() {
* throw sneakyThrow(new IOException("You don't need to catch me!"));
* }</pre>
* <p>
* NB: The exception is not wrapped, ignored, swallowed, or redefined. The JVM actually does not know or care
* about the concept of a 'checked exception'. All this method does is hide the act of throwing a checked exception
* from the java compiler.
* <p>
* Note that this method has a return type of {@code RuntimeException}; it is advised you always call this
* method as argument to the {@code throw} statement to avoid compiler errors regarding no return
* statement and similar problems. This method won't of course return an actual {@code RuntimeException} -
* it never returns, it always throws the provided exception.
*
* @param t The throwable to throw without requiring you to catch its type.
* @return A dummy RuntimeException; this method never returns normally, it <em>always</em> throws an exception!
*/
public static RuntimeException sneakyThrow(Throwable t) {
if (t == null) throw new NullPointerException("t");
return Lombok.<RuntimeException>sneakyThrow0(t);
}

// 对返回参数做了强转为T类型
private static <T extends Throwable> T sneakyThrow0(Throwable t) throws T {
throw (T)t;
}

整个处理过程中, 最重要的是throw (T)t, 使用泛型,将传入的Throwable强转为RuntimeException异常.

虽然, 我们抛出的异常不是RuntimeException,但是可以骗过javac编译器,泛型最后存储为字节码文件时并没有泛型信息.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

快速上手 ClickHouse

发表于 2021-10-16

本篇来自数月前对外分享的文稿整理,并进行了一些扩展。

希望通过简单的方式,来介绍新手如何一步一步上手 ClickHouse,如果你有潜在的数据分析的需求,但是不知道从哪里开始,那么希望本文能够帮助到你。

写在前面

关于 ClickHouse 在追求性能的场景下的溢美之词,我觉得没有必要再重复了。以过往经验来看,你可以使用极其低的成本来完成以往 RDBMS(比如MySQL)做不到的准实时级别的数据分析,也可以用它来做远程多个数据库实例的数据迁移或者归档存储。

感谢两年前一位好朋友对我进行的技术选型推荐,使用 ClickHouse 可以简化非常多的不必要的基础设施搭建和维护。我们曾搭建过一台比较奢华的机器(256核心512GB内存)来进行准实时的数据分析(花费万分之几秒从海量数据中查结果),以及支持每秒落地几十万条以上数据,而丝毫不影响服务器查询性能;也曾实践过从两千块的 NUC 上跑边缘计算任务,相对快速的拿到需要分析的结果(花费千分之一到百分之一秒),以及在16核心64GB内存的普通笔记本上,跑超过十亿数据集的复杂计算的尝试(分钟级)。

之前使用过的“豪华”配置

所以,如果你有以下需求,ClickHouse 可能也会非常适合你:

  • 快速分析一些离线数据,做数据计算、聚合、筛选。
  • 有大量读取需求,并且针对原始数据修改的需求非常少,如果存在这类需求,可以接受“追加数据”配合“版本过滤”的方式处理。
  • 数据字段比较丰富,数据存在非常多“列”。
  • 业务并发需求不高,查询者(消费者)只有几个或者一两百个以下。

先来聊聊硬件选择。

硬件选择策略

会考虑选择 ClickHouse 的同学,一般应该是遇到了当前业务,到了需要或者不得不“考虑效率”的时刻。

一般情况,很少有需要直接把 ClickHouse 返回数据作为同步结果直接返回给调用方的场景,勤俭节约的程序员们一般都会使用异步模式,所以在极少并发的情况下,我们对于 ClickHouse 的硬件要求也就越来越低了: 亿级别以下的数据,最低只要 4核心16GB 的虚拟机也能轻松搞定;而亿级别到百亿级别的数据,只要你能搞定32~64G内存,计算出来的时间也只几乎只和你设备的核心数数量、CPU缓存大小是多少有关而已 。

所以,在考虑使用 ClickHouse 的时候,如果你是用来做一个快速或者相对快速的“离线”数据分析,那么优先需要考虑的是你的数据量有多大,以及需要满足快速计算的内存门槛下限是否足够,接着才是考虑你需要多快的拿到计算结果,尽量在成本预算之内,优先选择拥有更多的核心数的 CPU、以及更大的 CPU 缓存 。至于 Cluster 模式,除非你需要提供实时接口,对于服务可用性有极高依赖和要求,有特别大的数据写入压力,不然默认情况是不需要配置的。当然,如果你有需求配置 Cluster,不推荐使用默认的分布式模式,因为数据并非完整镜像,而是均匀分布在每一个节点,如果某一个节点跪掉,你将“实时”损失 N 分之 1 的数据,导致最终计算结果不能说不准确,只能说压根可能是错的。官网为此推出了一个“Replicated”的数据库引擎,这个数据库引擎基于 Atomic 引擎,借助 ZooKeeper 进行完整的数据复制,虽然目前还处于实验阶段,但是总比“丢数据”强吧。

除此之外,还有一个因素会极大的影响 ClickHouse 帮助我们拿到计算结果的时间,就是存储介质,这里推荐使用 SSD 作为存储介质,如果你是用于小样本分析,甚至可以使用 TB 规格、便宜的民用存储。如果追求极致成本,甚至可以参考我之前的内容《廉价的家用工作站方案:前篇》、《NUC 折腾笔记 - 储存能力测试》,如果你是进行高频次、海量数据的计算,有比较大的存储量下限要求和可预期的大容量数据增长,考虑到成本和更高的数据存储可靠性,Raid 50 模式的机械磁盘会更适合你。

当然,如果你目前啥都没有,只是用于学习,本地起一个 Docker 容器,也能开始学习之旅,以及百万到千万级别的数据计算和分析。

软件环境选择

我目前所有的机器都运行在 Ubuntu + 容器环境,为什么这么选择呢,因为“Ubuntu 是容器世界里的一等公民”,本文考虑到快速上手,也同样选择使用套环境。

当然,如果你选择裸机直接安装 ClickHouse,使用更稳定的 Debian 也是个不错的选择,至于 CentOS ,时至今日,真的是没有推荐的理由和必要了(企业付费购买 RHEL 是另外一个话题)。

在容器环境内跑 ClickHouse 会损失比较多的“转换”性能,在存储和网络转发上都会存在一定的体现,所以实际生产环境能够裸机安装的,请脱离容器使用。

如果你已经安装好了 Docker环境,那么我们可以继续下一个章节啦。如果你还不熟悉如何安装 Docker,可以参考本站知识地图中的关于容器安装的内容,自行了解学习。

前置准备:测试使用的数据集

为了熟悉和了解基础语法和进行 ClickHouse 高性能体验,我们可以先使用官方提供的 Yandex.Metrica Data 来进行试验。(更多的性能测试,可以从官方仓库的 测试数据集 中了解)

  1. https://datasets.clickhouse.tech/hits/partitions/hits_v1.tar
  2. https://datasets.clickhouse.tech/visits/partitions/visits_v1.tar

此外,为了演示如何在不纠结数据类型转换的情况下,快速完成数据导入,我们还需要使用一个传统类型的数据库的数据集进行操作,这里选择网友开源项目中使用的“人人影视”数据库(MySQL) https://yyets.dmesg.app/database 。

数据下载完毕之后,我们需要先对数据进行解压缩。

1
2
3
bash复制代码mkdir data
tar xvf hits_v1.tar -C data
tar xvf visits_v1.tar -C data

通过 du 命令可以看到使用的数据实际使用了 1.7GB空间,顺便提一下,这些数据如果存储在 MySQL 中,存储空间可能会膨胀 3~5倍以上。

1
2
bash复制代码du -hs data
1.7G data

数据解压完毕,就可以开始准备对 ClickHouse 的容器运行配置了。

前置准备:准备 ClickHouse 运行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
yaml复制代码version: "2"

services:

server:
image: yandex/clickhouse-server:21.9.4.35
container_name: clickhouse
expose:
- 9000
- 8123
- 9009
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
environment:
- TZ=Asia/Shanghai
# - CLICKHOUSE_USER=root
# - CLICKHOUSE_PASSWORD=xmnzdwH5
volumes:
- ./data:/var/lib/clickhouse
# 按需使用
# - ./config.xml:/etc/clickhouse-server/config.xml
# - ./users.xml:/etc/clickhouse-server/users.xml

将上面的配置保存为 docker-compose.yml,并使用 docker-compose up -d 启动 ClickHouse,以备稍后使用。

额外说一下,ClickHouse 的版本更新很快,建议升级的时候先做一些小样本测试,测试常用场景是否正常,再进行版本更新替换。

ClickHouse 初体验

ClickHouse 使用的 SQL 语法相比较 MySQL 等数据库会宽松许多,类比的话,就像是之前写 Java 的选手一下子步入了 Python 和 JavaScript 的世界。

因为使用容器启动 ClickHouse,所以我们可以通过 docker exec 命令进入 ClickHouse 的交互式终端。

1
bash复制代码docker exec -it clickhouse clickhouse-client

进入终端后,先来看看有哪些“数据库”和数据表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
bash复制代码# 查看数据库
cc1b062138da :) show databases

SHOW DATABASES

Query id: efaa1c51-e112-43d6-b803-1e6dd86ad43b

┌─name─────┐
│ datasets │
│ default │
│ system │
└──────────┘

3 rows in set. Elapsed: 0.003 sec.

# 切换数据库
cc1b062138da :) use datasets

USE datasets

Query id: b10ff8f3-0743-42f4-9ee1-663b9a2c4955

Ok.

0 rows in set. Elapsed: 0.002 sec.

# 查看数据表
cc1b062138da :) show tables

SHOW TABLES

Query id: c6eb8203-6ea2-4576-9bb7-74ad4e1c7de9

┌─name──────┐
│ hits_v1 │
│ visits_v1 │
└───────────┘

2 rows in set. Elapsed: 0.005 sec.

上面的结果中的 datasets 就是我们导入的数据集。ClickHouse 对于数据存放比较“佛系”,如果你查看本地目录可以看到上面的数据和 data/datasets 目录保持一致,实际操作使用的时候,只要把 data 目录打个压缩包就能完成数据备份了,是不是很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
bash复制代码 tree -L 3 data/data/datasets 
data/data/datasets
├── hits_v1
│ ├── 201403_10_18_2
│ │ ├── AdvEngineID.bin
│ │ ├── AdvEngineID.mrk
│ │ ├── Age.bin
│ │ ├── Age.mrk
│ │ ├── UserID.bin
│ │ ├── UserID.mrk
...
│ │ ├── WatchID.bin
│ │ ├── WatchID.mrk
│ │ ├── YCLID.bin
│ │ ├── YCLID.mrk
│ │ ├── checksums.txt
│ │ ├── columns.txt
│ │ ├── count.txt
│ │ ├── minmax_EventDate.idx
│ │ ├── partition.dat
│ │ └── primary.idx
│ ├── detached
│ └── format_version.txt
└── visits_v1
├── 20140317_20140323_3_4_1
│ ├── AdvEngineID.bin
│ ├── AdvEngineID.mrk
│ ├── Age.bin
│ ├── Age.mrk
│ ├── Attendance.bin
│ ├── Attendance.mrk
...
│ ├── ClickBannerID.bin
│ ├── ClickBannerID.mrk
│ ├── ClickClientIP.bin
│ ├── ClickClientIP.mrk
│ ├── YCLID.bin
│ ├── YCLID.mrk
│ ├── checksums.txt
│ ├── columns.txt
│ └── primary.idx
├── detached
└── format_version.txt

6 directories, 675 files

为了后续敲的命令能简单些,我们针对数据表先进行一个重命名操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
bash复制代码# 分别去掉两张表的版本后缀

cc1b062138da :) rename table hits_v1 to hits

RENAME TABLE hits_v1 TO hits

Query id: dba1405a-1836-4d5a-af23-3ce0f5b31d41

Ok.

0 rows in set. Elapsed: 0.014 sec.



cc1b062138da :) rename table visits_v1 to visits

RENAME TABLE visits_v1 TO visits

Query id: 9ffc039c-86c3-42a9-91a6-3ed165254e0b

Ok.

0 rows in set. Elapsed: 0.012 sec.


# 再次查看数据表
cc1b062138da :) show tables

SHOW TABLES

Query id: da91fb7c-5224-4c7f-9a6c-ce4cf37f9fa8

┌─name───┐
│ hits │
│ visits │
└────────┘

2 rows in set. Elapsed: 0.004 sec.

将数据表重命名之后,接下来,来看看这两张表里到底有多少数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码SELECT
hits,
visits
FROM
(
SELECT count() AS hits
FROM hits
) AS table_hits
,
(
SELECT count() AS visits
FROM visits
) AS table_visits

可以看到两张表数据量都不大,百万到千万级别。

1
2
3
4
5
sql复制代码┌────hits─┬──visits─┐
│ 8873898 │ 1676861 │
└─────────┴─────────┘

1 rows in set. Elapsed: 0.005 sec.

接着我们来查看一下两张表的表结构,可以看到两张表,分别有133个、181个列,是“一般意义上的”宽表,非常适合进行分析使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
sql复制代码desc hits


cc1b062138da :) desc hits
:-]
:-]

DESCRIBE TABLE hits

Query id: b8d8b650-2395-4207-b2ce-4200dc9c0fce

┌─name───────────────────────┬─type────────────┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐
│ WatchID │ UInt64 │ │ │ │ │ │
│ JavaEnable │ UInt8 │ │ │ │ │ │
│ Title │ String │ │ │ │ │ │
│ GoodEvent │ Int16 │ │ │ │ │ │
│ EventTime │ DateTime │ │ │ │ │ │
│ EventDate │ Date │ │ │ │ │ │
│ CounterID │ UInt32 │ │ │ │ │ │
│ ClientIP │ UInt32 │ │ │ │ │ │
│ ClientIP6 │ FixedString(16) │ │ │ │ │ │
│ RegionID │ UInt32 │ │ │ │ │ │
│ UserID │ UInt64 │ │ │ │ │ │
│ CounterClass │ Int8 │ │ │ │ │ │
│ OS │ UInt8 │ │ │ │ │ │
│ UserAgent │ UInt8 │ │ │ │ │ │
│ URL │ String │ │ │ │ │ │
│ Referer │ String │ │ │ │ │ │
│ URLDomain │ String │ │ │ │ │ │
│ RefererDomain │ String │ │ │ │ │ │
│ Refresh │ UInt8 │ │ │ │ │ │
│ IsRobot │ UInt8 │ │ │ │ │ │
│ RefererCategories │ Array(UInt16) │ │ │ │ │ │
│ URLCategories │ Array(UInt16) │ │ │ │ │ │
│ URLRegions │ Array(UInt32) │ │ │ │ │ │
│ RefererRegions │ Array(UInt32) │ │ │ │ │ │
│ ResolutionWidth │ UInt16 │ │ │ │ │ │
│ ResolutionHeight │ UInt16 │ │ │ │ │ │
│ ResolutionDepth │ UInt8 │ │ │ │ │ │
│ FlashMajor │ UInt8 │ │ │ │ │ │
│ FlashMinor │ UInt8 │ │ │ │ │ │
│ FlashMinor2 │ String │ │ │ │ │ │
│ NetMajor │ UInt8 │ │ │ │ │ │
│ NetMinor │ UInt8 │ │ │ │ │ │
│ UserAgentMajor │ UInt16 │ │ │ │ │ │
│ UserAgentMinor │ FixedString(2) │ │ │ │ │ │
│ CookieEnable │ UInt8 │ │ │ │ │ │
│ JavascriptEnable │ UInt8 │ │ │ │ │ │
│ IsMobile │ UInt8 │ │ │ │ │ │
│ MobilePhone │ UInt8 │ │ │ │ │ │
│ MobilePhoneModel │ String │ │ │ │ │ │
│ Params │ String │ │ │ │ │ │
│ IPNetworkID │ UInt32 │ │ │ │ │ │
│ TraficSourceID │ Int8 │ │ │ │ │ │
│ SearchEngineID │ UInt16 │ │ │ │ │ │
│ SearchPhrase │ String │ │ │ │ │ │
│ AdvEngineID │ UInt8 │ │ │ │ │ │
│ IsArtifical │ UInt8 │ │ │ │ │ │
│ WindowClientWidth │ UInt16 │ │ │ │ │ │
│ WindowClientHeight │ UInt16 │ │ │ │ │ │
│ ClientTimeZone │ Int16 │ │ │ │ │ │
│ ClientEventTime │ DateTime │ │ │ │ │ │
│ SilverlightVersion1 │ UInt8 │ │ │ │ │ │
│ SilverlightVersion2 │ UInt8 │ │ │ │ │ │
│ SilverlightVersion3 │ UInt32 │ │ │ │ │ │
│ SilverlightVersion4 │ UInt16 │ │ │ │ │ │
│ PageCharset │ String │ │ │ │ │ │
│ CodeVersion │ UInt32 │ │ │ │ │ │
│ IsLink │ UInt8 │ │ │ │ │ │
│ IsDownload │ UInt8 │ │ │ │ │ │
│ IsNotBounce │ UInt8 │ │ │ │ │ │
│ FUniqID │ UInt64 │ │ │ │ │ │
│ HID │ UInt32 │ │ │ │ │ │
│ IsOldCounter │ UInt8 │ │ │ │ │ │
│ IsEvent │ UInt8 │ │ │ │ │ │
│ IsParameter │ UInt8 │ │ │ │ │ │
│ DontCountHits │ UInt8 │ │ │ │ │ │
│ WithHash │ UInt8 │ │ │ │ │ │
│ HitColor │ FixedString(1) │ │ │ │ │ │
│ UTCEventTime │ DateTime │ │ │ │ │ │
│ Age │ UInt8 │ │ │ │ │ │
│ Sex │ UInt8 │ │ │ │ │ │
│ Income │ UInt8 │ │ │ │ │ │
│ Interests │ UInt16 │ │ │ │ │ │
│ Robotness │ UInt8 │ │ │ │ │ │
│ GeneralInterests │ Array(UInt16) │ │ │ │ │ │
│ RemoteIP │ UInt32 │ │ │ │ │ │
│ RemoteIP6 │ FixedString(16) │ │ │ │ │ │
│ WindowName │ Int32 │ │ │ │ │ │
│ OpenerName │ Int32 │ │ │ │ │ │
│ HistoryLength │ Int16 │ │ │ │ │ │
│ BrowserLanguage │ FixedString(2) │ │ │ │ │ │
│ BrowserCountry │ FixedString(2) │ │ │ │ │ │
│ SocialNetwork │ String │ │ │ │ │ │
│ SocialAction │ String │ │ │ │ │ │
│ HTTPError │ UInt16 │ │ │ │ │ │
│ SendTiming │ Int32 │ │ │ │ │ │
│ DNSTiming │ Int32 │ │ │ │ │ │
│ ConnectTiming │ Int32 │ │ │ │ │ │
│ ResponseStartTiming │ Int32 │ │ │ │ │ │
│ ResponseEndTiming │ Int32 │ │ │ │ │ │
│ FetchTiming │ Int32 │ │ │ │ │ │
│ RedirectTiming │ Int32 │ │ │ │ │ │
│ DOMInteractiveTiming │ Int32 │ │ │ │ │ │
│ DOMContentLoadedTiming │ Int32 │ │ │ │ │ │
│ DOMCompleteTiming │ Int32 │ │ │ │ │ │
│ LoadEventStartTiming │ Int32 │ │ │ │ │ │
│ LoadEventEndTiming │ Int32 │ │ │ │ │ │
│ NSToDOMContentLoadedTiming │ Int32 │ │ │ │ │ │
│ FirstPaintTiming │ Int32 │ │ │ │ │ │
│ RedirectCount │ Int8 │ │ │ │ │ │
│ SocialSourceNetworkID │ UInt8 │ │ │ │ │ │
│ SocialSourcePage │ String │ │ │ │ │ │
│ ParamPrice │ Int64 │ │ │ │ │ │
│ ParamOrderID │ String │ │ │ │ │ │
│ ParamCurrency │ FixedString(3) │ │ │ │ │ │
│ ParamCurrencyID │ UInt16 │ │ │ │ │ │
│ GoalsReached │ Array(UInt32) │ │ │ │ │ │
│ OpenstatServiceName │ String │ │ │ │ │ │
│ OpenstatCampaignID │ String │ │ │ │ │ │
│ OpenstatAdID │ String │ │ │ │ │ │
│ OpenstatSourceID │ String │ │ │ │ │ │
│ UTMSource │ String │ │ │ │ │ │
│ UTMMedium │ String │ │ │ │ │ │
│ UTMCampaign │ String │ │ │ │ │ │
│ UTMContent │ String │ │ │ │ │ │
│ UTMTerm │ String │ │ │ │ │ │
│ FromTag │ String │ │ │ │ │ │
│ HasGCLID │ UInt8 │ │ │ │ │ │
│ RefererHash │ UInt64 │ │ │ │ │ │
│ URLHash │ UInt64 │ │ │ │ │ │
│ CLID │ UInt32 │ │ │ │ │ │
│ YCLID │ UInt64 │ │ │ │ │ │
│ ShareService │ String │ │ │ │ │ │
│ ShareURL │ String │ │ │ │ │ │
│ ShareTitle │ String │ │ │ │ │ │
│ ParsedParams.Key1 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key2 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key3 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key4 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key5 │ Array(String) │ │ │ │ │ │
│ ParsedParams.ValueDouble │ Array(Float64) │ │ │ │ │ │
│ IslandID │ FixedString(16) │ │ │ │ │ │
│ RequestNum │ UInt32 │ │ │ │ │ │
│ RequestTry │ UInt8 │ │ │ │ │ │
└────────────────────────────┴─────────────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘

133 rows in set. Elapsed: 0.003 sec.

cc1b062138da :)

再来看看另外一张表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
sql复制代码cc1b062138da :) desc visits

DESCRIBE TABLE visits

Query id: 821e1692-b571-42ad-a38f-88bd120e478d

┌─name───────────────────────────────┬─type────────────┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐
│ CounterID │ UInt32 │ │ │ │ │ │
│ StartDate │ Date │ │ │ │ │ │
│ Sign │ Int8 │ │ │ │ │ │
│ IsNew │ UInt8 │ │ │ │ │ │
│ VisitID │ UInt64 │ │ │ │ │ │
│ UserID │ UInt64 │ │ │ │ │ │
│ StartTime │ DateTime │ │ │ │ │ │
│ Duration │ UInt32 │ │ │ │ │ │
│ UTCStartTime │ DateTime │ │ │ │ │ │
│ PageViews │ Int32 │ │ │ │ │ │
│ Hits │ Int32 │ │ │ │ │ │
│ IsBounce │ UInt8 │ │ │ │ │ │
│ Referer │ String │ │ │ │ │ │
│ StartURL │ String │ │ │ │ │ │
│ RefererDomain │ String │ │ │ │ │ │
│ StartURLDomain │ String │ │ │ │ │ │
│ EndURL │ String │ │ │ │ │ │
│ LinkURL │ String │ │ │ │ │ │
│ IsDownload │ UInt8 │ │ │ │ │ │
│ TraficSourceID │ Int8 │ │ │ │ │ │
│ SearchEngineID │ UInt16 │ │ │ │ │ │
│ SearchPhrase │ String │ │ │ │ │ │
│ AdvEngineID │ UInt8 │ │ │ │ │ │
│ PlaceID │ Int32 │ │ │ │ │ │
│ RefererCategories │ Array(UInt16) │ │ │ │ │ │
│ URLCategories │ Array(UInt16) │ │ │ │ │ │
│ URLRegions │ Array(UInt32) │ │ │ │ │ │
│ RefererRegions │ Array(UInt32) │ │ │ │ │ │
│ IsYandex │ UInt8 │ │ │ │ │ │
│ GoalReachesDepth │ Int32 │ │ │ │ │ │
│ GoalReachesURL │ Int32 │ │ │ │ │ │
│ GoalReachesAny │ Int32 │ │ │ │ │ │
│ SocialSourceNetworkID │ UInt8 │ │ │ │ │ │
│ SocialSourcePage │ String │ │ │ │ │ │
│ MobilePhoneModel │ String │ │ │ │ │ │
│ ClientEventTime │ DateTime │ │ │ │ │ │
│ RegionID │ UInt32 │ │ │ │ │ │
│ ClientIP │ UInt32 │ │ │ │ │ │
│ ClientIP6 │ FixedString(16) │ │ │ │ │ │
│ RemoteIP │ UInt32 │ │ │ │ │ │
│ RemoteIP6 │ FixedString(16) │ │ │ │ │ │
│ IPNetworkID │ UInt32 │ │ │ │ │ │
│ SilverlightVersion3 │ UInt32 │ │ │ │ │ │
│ CodeVersion │ UInt32 │ │ │ │ │ │
│ ResolutionWidth │ UInt16 │ │ │ │ │ │
│ ResolutionHeight │ UInt16 │ │ │ │ │ │
│ UserAgentMajor │ UInt16 │ │ │ │ │ │
│ UserAgentMinor │ UInt16 │ │ │ │ │ │
│ WindowClientWidth │ UInt16 │ │ │ │ │ │
│ WindowClientHeight │ UInt16 │ │ │ │ │ │
│ SilverlightVersion2 │ UInt8 │ │ │ │ │ │
│ SilverlightVersion4 │ UInt16 │ │ │ │ │ │
│ FlashVersion3 │ UInt16 │ │ │ │ │ │
│ FlashVersion4 │ UInt16 │ │ │ │ │ │
│ ClientTimeZone │ Int16 │ │ │ │ │ │
│ OS │ UInt8 │ │ │ │ │ │
│ UserAgent │ UInt8 │ │ │ │ │ │
│ ResolutionDepth │ UInt8 │ │ │ │ │ │
│ FlashMajor │ UInt8 │ │ │ │ │ │
│ FlashMinor │ UInt8 │ │ │ │ │ │
│ NetMajor │ UInt8 │ │ │ │ │ │
│ NetMinor │ UInt8 │ │ │ │ │ │
│ MobilePhone │ UInt8 │ │ │ │ │ │
│ SilverlightVersion1 │ UInt8 │ │ │ │ │ │
│ Age │ UInt8 │ │ │ │ │ │
│ Sex │ UInt8 │ │ │ │ │ │
│ Income │ UInt8 │ │ │ │ │ │
│ JavaEnable │ UInt8 │ │ │ │ │ │
│ CookieEnable │ UInt8 │ │ │ │ │ │
│ JavascriptEnable │ UInt8 │ │ │ │ │ │
│ IsMobile │ UInt8 │ │ │ │ │ │
│ BrowserLanguage │ UInt16 │ │ │ │ │ │
│ BrowserCountry │ UInt16 │ │ │ │ │ │
│ Interests │ UInt16 │ │ │ │ │ │
│ Robotness │ UInt8 │ │ │ │ │ │
│ GeneralInterests │ Array(UInt16) │ │ │ │ │ │
│ Params │ Array(String) │ │ │ │ │ │
│ Goals.ID │ Array(UInt32) │ │ │ │ │ │
│ Goals.Serial │ Array(UInt32) │ │ │ │ │ │
│ Goals.EventTime │ Array(DateTime) │ │ │ │ │ │
│ Goals.Price │ Array(Int64) │ │ │ │ │ │
│ Goals.OrderID │ Array(String) │ │ │ │ │ │
│ Goals.CurrencyID │ Array(UInt32) │ │ │ │ │ │
│ WatchIDs │ Array(UInt64) │ │ │ │ │ │
│ ParamSumPrice │ Int64 │ │ │ │ │ │
│ ParamCurrency │ FixedString(3) │ │ │ │ │ │
│ ParamCurrencyID │ UInt16 │ │ │ │ │ │
│ ClickLogID │ UInt64 │ │ │ │ │ │
│ ClickEventID │ Int32 │ │ │ │ │ │
│ ClickGoodEvent │ Int32 │ │ │ │ │ │
│ ClickEventTime │ DateTime │ │ │ │ │ │
│ ClickPriorityID │ Int32 │ │ │ │ │ │
│ ClickPhraseID │ Int32 │ │ │ │ │ │
│ ClickPageID │ Int32 │ │ │ │ │ │
│ ClickPlaceID │ Int32 │ │ │ │ │ │
│ ClickTypeID │ Int32 │ │ │ │ │ │
│ ClickResourceID │ Int32 │ │ │ │ │ │
│ ClickCost │ UInt32 │ │ │ │ │ │
│ ClickClientIP │ UInt32 │ │ │ │ │ │
│ ClickDomainID │ UInt32 │ │ │ │ │ │
│ ClickURL │ String │ │ │ │ │ │
│ ClickAttempt │ UInt8 │ │ │ │ │ │
│ ClickOrderID │ UInt32 │ │ │ │ │ │
│ ClickBannerID │ UInt32 │ │ │ │ │ │
│ ClickMarketCategoryID │ UInt32 │ │ │ │ │ │
│ ClickMarketPP │ UInt32 │ │ │ │ │ │
│ ClickMarketCategoryName │ String │ │ │ │ │ │
│ ClickMarketPPName │ String │ │ │ │ │ │
│ ClickAWAPSCampaignName │ String │ │ │ │ │ │
│ ClickPageName │ String │ │ │ │ │ │
│ ClickTargetType │ UInt16 │ │ │ │ │ │
│ ClickTargetPhraseID │ UInt64 │ │ │ │ │ │
│ ClickContextType │ UInt8 │ │ │ │ │ │
│ ClickSelectType │ Int8 │ │ │ │ │ │
│ ClickOptions │ String │ │ │ │ │ │
│ ClickGroupBannerID │ Int32 │ │ │ │ │ │
│ OpenstatServiceName │ String │ │ │ │ │ │
│ OpenstatCampaignID │ String │ │ │ │ │ │
│ OpenstatAdID │ String │ │ │ │ │ │
│ OpenstatSourceID │ String │ │ │ │ │ │
│ UTMSource │ String │ │ │ │ │ │
│ UTMMedium │ String │ │ │ │ │ │
│ UTMCampaign │ String │ │ │ │ │ │
│ UTMContent │ String │ │ │ │ │ │
│ UTMTerm │ String │ │ │ │ │ │
│ FromTag │ String │ │ │ │ │ │
│ HasGCLID │ UInt8 │ │ │ │ │ │
│ FirstVisit │ DateTime │ │ │ │ │ │
│ PredLastVisit │ Date │ │ │ │ │ │
│ LastVisit │ Date │ │ │ │ │ │
│ TotalVisits │ UInt32 │ │ │ │ │ │
│ TraficSource.ID │ Array(Int8) │ │ │ │ │ │
│ TraficSource.SearchEngineID │ Array(UInt16) │ │ │ │ │ │
│ TraficSource.AdvEngineID │ Array(UInt8) │ │ │ │ │ │
│ TraficSource.PlaceID │ Array(UInt16) │ │ │ │ │ │
│ TraficSource.SocialSourceNetworkID │ Array(UInt8) │ │ │ │ │ │
│ TraficSource.Domain │ Array(String) │ │ │ │ │ │
│ TraficSource.SearchPhrase │ Array(String) │ │ │ │ │ │
│ TraficSource.SocialSourcePage │ Array(String) │ │ │ │ │ │
│ Attendance │ FixedString(16) │ │ │ │ │ │
│ CLID │ UInt32 │ │ │ │ │ │
│ YCLID │ UInt64 │ │ │ │ │ │
│ NormalizedRefererHash │ UInt64 │ │ │ │ │ │
│ SearchPhraseHash │ UInt64 │ │ │ │ │ │
│ RefererDomainHash │ UInt64 │ │ │ │ │ │
│ NormalizedStartURLHash │ UInt64 │ │ │ │ │ │
│ StartURLDomainHash │ UInt64 │ │ │ │ │ │
│ NormalizedEndURLHash │ UInt64 │ │ │ │ │ │
│ TopLevelDomain │ UInt64 │ │ │ │ │ │
│ URLScheme │ UInt64 │ │ │ │ │ │
│ OpenstatServiceNameHash │ UInt64 │ │ │ │ │ │
│ OpenstatCampaignIDHash │ UInt64 │ │ │ │ │ │
│ OpenstatAdIDHash │ UInt64 │ │ │ │ │ │
│ OpenstatSourceIDHash │ UInt64 │ │ │ │ │ │
│ UTMSourceHash │ UInt64 │ │ │ │ │ │
│ UTMMediumHash │ UInt64 │ │ │ │ │ │
│ UTMCampaignHash │ UInt64 │ │ │ │ │ │
│ UTMContentHash │ UInt64 │ │ │ │ │ │
│ UTMTermHash │ UInt64 │ │ │ │ │ │
│ FromHash │ UInt64 │ │ │ │ │ │
│ WebVisorEnabled │ UInt8 │ │ │ │ │ │
│ WebVisorActivity │ UInt32 │ │ │ │ │ │
│ ParsedParams.Key1 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key2 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key3 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key4 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key5 │ Array(String) │ │ │ │ │ │
│ ParsedParams.ValueDouble │ Array(Float64) │ │ │ │ │ │
│ Market.Type │ Array(UInt8) │ │ │ │ │ │
│ Market.GoalID │ Array(UInt32) │ │ │ │ │ │
│ Market.OrderID │ Array(String) │ │ │ │ │ │
│ Market.OrderPrice │ Array(Int64) │ │ │ │ │ │
│ Market.PP │ Array(UInt32) │ │ │ │ │ │
│ Market.DirectPlaceID │ Array(UInt32) │ │ │ │ │ │
│ Market.DirectOrderID │ Array(UInt32) │ │ │ │ │ │
│ Market.DirectBannerID │ Array(UInt32) │ │ │ │ │ │
│ Market.GoodID │ Array(String) │ │ │ │ │ │
│ Market.GoodName │ Array(String) │ │ │ │ │ │
│ Market.GoodQuantity │ Array(Int32) │ │ │ │ │ │
│ Market.GoodPrice │ Array(Int64) │ │ │ │ │ │
│ IslandID │ FixedString(16) │ │ │ │ │ │
└────────────────────────────────────┴─────────────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘

181 rows in set. Elapsed: 0.004 sec.

数据查询操作

接下来,我们来针对这两张表里的数据进行一些基础的查询操作。

分组查询取Top N记录

日常业务中比较频繁的一个操作是有一大堆数据,我们需要筛选其中比较“热门”的数据,假设我们要统计网站具备不同“计数器 ID”的子模块的访问次数,那么可以通过下面的语句来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sql复制代码cc1b062138da :) SELECT CounterID, count() AS nums FROM hits GROUP BY CounterID ORDER BY nums DESC LIMIT 10;

SELECT
CounterID,
count() AS nums
FROM hits
GROUP BY CounterID
ORDER BY nums DESC
LIMIT 10

Query id: 4206896d-e52e-48fc-bad1-3fe60949f90a

┌─CounterID─┬───nums─┐
│ 1704509 │ 523264 │
│ 732797 │ 475698 │
│ 598875 │ 337212 │
│ 792887 │ 252197 │
│ 3807842 │ 196036 │
│ 25703952 │ 147211 │
│ 716829 │ 90109 │
│ 59183 │ 85379 │
│ 33010362 │ 77807 │
│ 800784 │ 77492 │
└───────────┴────────┘

10 rows in set. Elapsed: 0.032 sec. Processed 8.87 million rows, 35.50 MB (277.95 million rows/s., 1.11 GB/s.)

或者如果我们要实现一个诸如“新浪、知乎搜索热榜”之类的功能,可以通过下面的方式,来展示前十条热搜关键词,以及对应的搜索次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
sql复制代码cc1b062138da :) SELECT SearchPhrase AS keyword, count() AS nums FROM hits GROUP BY keyword ORDER BY nums DESC, keyword LIMIT 10

SELECT
SearchPhrase AS keyword,
count() AS nums
FROM hits
GROUP BY keyword
ORDER BY
nums DESC,
keyword ASC
LIMIT 10

Query id: 62952a2b-858a-463d-b5d3-7af60b9f7e92

┌─keyword────────────────────┬────nums─┐
│ │ 8267016 │
│ ст 12.168.0.1 │ 3567 │
│ orton │ 2402 │
│ игры лица и гым чан дизайн │ 2166 │
│ imgsrc │ 1848 │
│ брызговик │ 1659 │
│ индийский афтозный │ 1549 │
│ ооооотводка и │ 1480 │
│ выступная мужчин │ 1247 │
│ юность │ 1112 │
└────────────────────────────┴─────────┘

10 rows in set. Elapsed: 0.096 sec. Processed 8.87 million rows, 112.70 MB (92.77 million rows/s., 1.18 GB/s.)

根据数值进行精确筛选

日常业务中,我们也很容易遇到需要看看到底有多少数值不为空的记录,查询方法类似 MySQL ,使用下面的语句可以快速统计 AdvEngineID 不为零的数据的总数:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码cc1b062138da :) SELECT count() FROM hits WHERE AdvEngineID != 0

SELECT count()
FROM hits
WHERE AdvEngineID != 0

Query id: e3bc420a-ef87-42a4-879e-bbf548d2e3a0

┌─count()─┐
│ 30641 │
└─────────┘

1 rows in set. Elapsed: 0.013 sec. Processed 8.87 million rows, 8.87 MB (668.11 million rows/s., 668.11 MB/s.)

数据计算

除了过滤掉为空的数据之外,还容易遇到需要对总访问次数进行求和的操作,以及计算所有来源的平均值,ClickHouse 中为我们提供了大量高性能的计算函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码cc1b062138da :) SELECT sum(AdvEngineID), count(), avg(ResolutionWidth) FROM hits

SELECT
sum(AdvEngineID),
count(),
avg(ResolutionWidth)
FROM hits

Query id: 47aaef91-69c9-49ed-8e2d-9801ec243338

┌─sum(AdvEngineID)─┬─count()─┬─avg(ResolutionWidth)─┐
│ 329039 │ 8873898 │ 1400.8565027454677 │
└──────────────────┴─────────┴──────────────────────┘

1 rows in set. Elapsed: 0.042 sec. Processed 8.87 million rows, 26.62 MB (210.68 million rows/s., 632.05 MB/s.)

数据去重

当然,有的时候,我们在应用侧会存储一些冗余的数据,为了计算的准确,我们需要以某个字段为准,进行去重操作,进行更精准的计算。

下面展示从“计数器ID”为 800784 的计数器中筛选数据,并根据 RegionID 字段进行分组,最终以降序排列展示去重后的用户ID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
sql复制代码cc1b062138da :) SELECT RegionID, uniq(UserID) AS UID FROM hits WHERE CounterID = 800784 GROUP BY RegionID ORDER BY UID DESC, RegionID LIMIT 10

SELECT
RegionID,
uniq(UserID) AS UID
FROM hits
WHERE CounterID = 800784
GROUP BY RegionID
ORDER BY
UID DESC,
RegionID ASC
LIMIT 10

Query id: 02049c4f-78e9-402a-93ab-1586c5d7a406

┌─RegionID─┬─UID─┐
│ 196 │ 559 │
│ 3 │ 161 │
│ 241 │ 147 │
│ 207 │ 106 │
│ 225 │ 62 │
│ 1 │ 60 │
│ 46 │ 59 │
│ 36 │ 58 │
│ 104 │ 58 │
│ 47 │ 57 │
└──────────┴─────┘

10 rows in set. Elapsed: 0.011 sec. Processed 81.92 thousand rows, 1.31 MB (7.61 million rows/s., 121.83 MB/s.)

再来一个更复杂一些的例子,你可以使用 uniqIf 同时设置多个去重条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码cc1b062138da :) SELECT uniq(UserID), uniqIf(UserID, CounterID = 800784), uniqIf(FUniqID, RegionID = 213) FROM hits

SELECT
uniq(UserID),
uniqIf(UserID, CounterID = 800784),
uniqIf(FUniqID, RegionID = 213)
FROM hits

Query id: 76839d79-9551-4863-a423-e0c21b2193fe

┌─uniq(UserID)─┬─uniqIf(UserID, equals(CounterID, 800784))─┬─uniqIf(FUniqID, equals(RegionID, 213))─┐
│ 120665 │ 4057 │ 106 │
└──────────────┴───────────────────────────────────────────┴────────────────────────────────────────┘

1 rows in set. Elapsed: 0.162 sec. Processed 8.87 million rows, 212.97 MB (54.87 million rows/s., 1.32 GB/s.)

数据排序

数据排序也是一个常见需求,比如我们根据数据时间,展示最近十条写入的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sql复制代码cc1b062138da :) SELECT EventTime FROM hits ORDER BY EventTime DESC LIMIT 10

SELECT EventTime
FROM hits
ORDER BY EventTime DESC
LIMIT 10

Query id: fbdc1df9-0ff1-408f-b681-3a94be1e6a86

┌───────────EventTime─┐
│ 2014-03-24 03:59:59 │
│ 2014-03-24 03:59:59 │
│ 2014-03-24 03:59:59 │
│ 2014-03-24 03:59:59 │
│ 2014-03-24 03:59:59 │
│ 2014-03-24 03:59:59 │
│ 2014-03-24 03:59:59 │
│ 2014-03-24 03:59:59 │
│ 2014-03-24 03:59:59 │
│ 2014-03-24 03:59:59 │
└─────────────────────┘

10 rows in set. Elapsed: 0.174 sec. Processed 8.87 million rows, 35.50 MB (51.11 million rows/s., 204.43 MB/s.)

复合查询条件的排序

ClickHouse 对于结构化的数据的查询支持也非常好,你可以使用类似下面的语句对于数组类型的数据进行查询,以及直接对结果进行排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
sql复制代码cc1b062138da :) SELECT ParsedParams.Key1 FROM visits FINAL WHERE VisitID != 0 AND notEmpty(ParsedParams.Key1) ORDER BY VisitID LIMIT 10

SELECT ParsedParams.Key1
FROM visits
FINAL
WHERE (VisitID != 0) AND notEmpty(ParsedParams.Key1)
ORDER BY VisitID ASC
LIMIT 10

Query id: 05a3ae18-43a5-414a-9c8a-8b98c6cbce42

┌─ParsedParams.Key1────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ['Кнопка'] │
│ ['pageParams','pageParams'] │
│ ['pageParams'] │
│ ['gen_time'] │
│ ['pageParams'] │
│ ['pageParams'] │
│ ['pageParams'] │
│ ['pageParams'] │
│ ['Прав','gen_timestamp','Прав','Действи','affili','gen_timestamp','Эксперимент про отрыв счетчика у папок','Просмотр писем'] │
│ ['Марка','gen_time','Марка'] │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

10 rows in set. Elapsed: 0.307 sec. Processed 1.68 million rows, 62.63 MB (5.46 million rows/s., 203.83 MB/s.)

范围查询

限制范围查询除了使用数学的比较符号外,一般分为两类,第一类是设置“白名单”进行查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码cc1b062138da :) select sum(Sign) from visits where CounterID in (942285);

SELECT sum(Sign)
FROM visits
WHERE CounterID IN (942285)

Query id: 44b2f770-8916-46b9-b056-74b24b155e95

┌─sum(Sign)─┐
│ 108133 │
└───────────┘

1 rows in set. Elapsed: 0.008 sec. Processed 114.69 thousand rows, 573.44 KB (14.42 million rows/s., 72.12 MB/s.)

第二类则是,根据给定的时间范围进行查询,在 ClickHouse 里,如果我们想知道一段数据的记录时间,甚至可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码cc1b062138da :) SELECT min(EventDate), max(EventDate) FROM hits

SELECT
min(EventDate),
max(EventDate)
FROM hits

Query id: dd1b7383-6846-4cf6-86cb-5217ffa1357f

┌─min(EventDate)─┬─max(EventDate)─┐
│ 2014-03-17 │ 2014-03-23 │
└────────────────┴────────────────┘

1 rows in set. Elapsed: 0.015 sec. Processed 8.87 million rows, 17.75 MB (599.25 million rows/s., 1.20 GB/s.)

Join 查询

联合查询也是一个常见需求,关于 Join ,一张图胜过千言万语。

About SQL JOINS

如果我们希望将一张百万,一张近千万的数据表进行联合查询,计算相同日期里,的用户UV和点击PV的话,可以这样操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
sql复制代码SELECT
EventDate,
hits,
visits
FROM
(
SELECT
EventDate,
count() AS hits
FROM hits
GROUP BY EventDate
)
ANY LEFT JOIN
(
SELECT
StartDate AS EventDate,
sum(Sign) AS visits
FROM visits
GROUP BY EventDate
) USING (EventDate)
ORDER BY hits DESC
LIMIT 10
SETTINGS joined_subquery_requires_alias = 0

Query id: dbfee618-01b1-44bf-af8f-12a73c7ff300

┌──EventDate─┬────hits─┬─visits─┐
│ 2014-03-17 │ 1406958 │ 265108 │
│ 2014-03-19 │ 1405797 │ 261624 │
│ 2014-03-18 │ 1383658 │ 258723 │
│ 2014-03-20 │ 1353623 │ 255328 │
│ 2014-03-21 │ 1245779 │ 236232 │
│ 2014-03-23 │ 1046491 │ 202212 │
│ 2014-03-22 │ 1031592 │ 197354 │
└────────────┴─────────┴────────┘

7 rows in set. Elapsed: 0.051 sec. Processed 10.55 million rows, 22.78 MB (207.12 million rows/s., 447.17 MB/s.)

子查询

子查询同样是一个非常常见的需求,可以帮助我们减少非常多不必要的手动步骤,比如像下面这样,先筛选一批用户,然后再计算这批用户的访问情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sql复制代码cc1b062138da :) SELECT count() FROM hits PREWHERE UserID IN (SELECT UserID FROM hits WHERE CounterID = 800784);

SELECT count()
FROM hits
PREWHERE UserID IN
(
SELECT UserID
FROM hits
WHERE CounterID = 800784
)

Query id: 5d254281-8c9e-4593-9d8a-bdce24037d97

┌─count()─┐
│ 1956422 │
└─────────┘

1 rows in set. Elapsed: 0.032 sec. Processed 8.96 million rows, 71.97 MB (283.90 million rows/s., 2.28 GB/s.)

cc1b062138da :)

上面的例子中,我们使用了 PREWHERE 来进行数据查询优化,可以过滤大量不必要的数据读取,不过如果你的数据量比较少,或者并发压力小,一律 WHERE 也问题不大,以下是两个语句对于数据读取量的差异:

1
2
3
4
5
sql复制代码# PREWHERE
1 rows in set. Elapsed: 0.106 sec. Processed 8.96 million rows, 71.97 MB (84.79 million rows/s., 681.46 MB/s.)

# WHERE
1 rows in set. Elapsed: 0.026 sec. Processed 8.96 million rows, 71.97 MB (339.11 million rows/s., 2.73 GB/s.)

字符串模糊搜索

有一些时候,我们需要进行快速的模糊搜索,ClickHouse 同样能够提供非常令人惊讶的性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码cc1b062138da :) SELECT count() FROM hits WHERE URL LIKE '%avtomobili%';

SELECT count()
FROM hits
WHERE URL LIKE '%avtomobili%'

Query id: 93ea9afc-070e-47ae-885e-6b14c26315a5

┌─count()─┐
│ 51354 │
└─────────┘

1 rows in set. Elapsed: 0.152 sec. Processed 8.87 million rows, 767.95 MB (58.44 million rows/s., 5.06 GB/s.)

彩蛋:偷懒画个图

除此之外,有的时候,我们会要求绘制一些图表来帮助我们直观的看到数据量级的差异,也可以使用 ClickHouse 自带的 bar 函数来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
sql复制代码cc1b062138da :) SELECT CounterID, count() AS c, bar(c, 0, 523264) FROM hits GROUP BY CounterID ORDER BY c DESC, CounterID ASC LIMIT 100;

SELECT
CounterID,
count() AS c,
bar(c, 0, 523264)
FROM hits
GROUP BY CounterID
ORDER BY
c DESC,
CounterID ASC
LIMIT 100

Query id: ee79be07-c93f-4aba-ad81-8d6938c99f96

┌─CounterID─┬──────c─┬─bar(count(), 0, 523264)──────────────────────────────────────────────────────────┐
│ 1704509 │ 523264 │ ████████████████████████████████████████████████████████████████████████████████ │
│ 732797 │ 475698 │ ████████████████████████████████████████████████████████████████████████▋ │
│ 598875 │ 337212 │ ███████████████████████████████████████████████████▌ │
│ 792887 │ 252197 │ ██████████████████████████████████████▌ │
│ 3807842 │ 196036 │ █████████████████████████████▊ │
│ 25703952 │ 147211 │ ██████████████████████▌ │
│ 716829 │ 90109 │ █████████████▋ │
│ 59183 │ 85379 │ █████████████ │
│ 33010362 │ 77807 │ ███████████▊ │
│ 800784 │ 77492 │ ███████████▋ │
│ 20810645 │ 73213 │ ███████████▏ │
│ 25843850 │ 68945 │ ██████████▌ │
│ 23447120 │ 67570 │ ██████████▎ │
│ 14739804 │ 64174 │ █████████▋ │
│ 32077710 │ 60456 │ █████████▏ │
│ 22446879 │ 58389 │ ████████▊ │
│ 170282 │ 57017 │ ████████▋ │
│ 11482817 │ 52345 │ ████████ │
│ 63469 │ 52142 │ ███████▊ │
│ 29103473 │ 47758 │ ███████▎ │
│ 10136747 │ 44080 │ ██████▋ │
│ 27528801 │ 43395 │ ██████▋ │
│ 10581377 │ 43279 │ ██████▌ │
│ 9841201 │ 40581 │ ██████▏ │
│ 20310963 │ 37562 │ █████▋ │
│ 17337667 │ 34301 │ █████▏ │
│ 28600281 │ 32776 │ █████ │
│ 32046685 │ 28788 │ ████▍ │
│ 10130880 │ 26603 │ ████ │
│ 8676831 │ 25733 │ ███▊ │
│ 53230 │ 25595 │ ███▊ │
│ 20271226 │ 25585 │ ███▊ │
│ 17420663 │ 25496 │ ███▊ │
│ 631207 │ 25270 │ ███▋ │
│ 633130 │ 24744 │ ███▋ │
│ 14324015 │ 23349 │ ███▌ │
│ 8537965 │ 21270 │ ███▎ │
│ 11285298 │ 20825 │ ███▏ │
│ 14937615 │ 20788 │ ███▏ │
│ 185050 │ 20785 │ ███▏ │
│ 16368233 │ 19897 │ ███ │
│ 81602 │ 19724 │ ███ │
│ 62896 │ 19717 │ ███ │
│ 12967664 │ 19402 │ ██▊ │
│ 15996597 │ 18557 │ ██▋ │
│ 4379238 │ 18370 │ ██▋ │
│ 90982 │ 17443 │ ██▋ │
│ 18211045 │ 17390 │ ██▋ │
│ 14625884 │ 17302 │ ██▋ │
│ 12864910 │ 17279 │ ██▋ │
│ 126096 │ 16959 │ ██▌ │
│ 30296134 │ 16849 │ ██▌ │
│ 26360482 │ 16175 │ ██▍ │
│ 17788950 │ 16017 │ ██▍ │
│ 5928716 │ 15340 │ ██▎ │
│ 15469035 │ 15171 │ ██▎ │
│ 29732125 │ 15146 │ ██▎ │
│ 32946244 │ 15104 │ ██▎ │
│ 20957241 │ 14719 │ ██▎ │
│ 9495695 │ 14584 │ ██▏ │
│ 29241146 │ 14540 │ ██▏ │
│ 109805 │ 14199 │ ██▏ │
│ 26905788 │ 13972 │ ██▏ │
│ 212019 │ 13930 │ ██▏ │
│ 171509 │ 13792 │ ██ │
│ 23913162 │ 13615 │ ██ │
│ 1861993 │ 13509 │ ██ │
│ 125776 │ 13308 │ ██ │
│ 11312316 │ 13181 │ ██ │
│ 32667326 │ 13181 │ ██ │
│ 28628973 │ 12922 │ █▊ │
│ 122804 │ 12520 │ █▊ │
│ 12322758 │ 12352 │ █▊ │
│ 1301819 │ 12283 │ █▊ │
│ 10769545 │ 12183 │ █▋ │
│ 21566939 │ 12170 │ █▋ │
│ 28905364 │ 12158 │ █▋ │
│ 4250765 │ 12049 │ █▋ │
│ 15009727 │ 11818 │ █▋ │
│ 12761932 │ 11733 │ █▋ │
│ 26995888 │ 11658 │ █▋ │
│ 12759346 │ 11514 │ █▋ │
│ 1507911 │ 11452 │ █▋ │
│ 968488 │ 11444 │ █▋ │
│ 15736172 │ 11358 │ █▋ │
│ 54310 │ 11193 │ █▋ │
│ 17027391 │ 11047 │ █▋ │
│ 17439919 │ 10936 │ █▋ │
│ 4480860 │ 10747 │ █▋ │
│ 26738469 │ 10738 │ █▋ │
│ 9986231 │ 10656 │ █▋ │
│ 1539995 │ 10655 │ █▋ │
│ 214556 │ 10625 │ █▌ │
│ 219339 │ 10522 │ █▌ │
│ 3266 │ 10503 │ █▌ │
│ 30563429 │ 10128 │ █▌ │
│ 1960469 │ 10098 │ █▌ │
│ 7901143 │ 10022 │ █▌ │
│ 194599 │ 9997 │ █▌ │
│ 21052498 │ 9780 │ █▍ │
└───────────┴────────┴──────────────────────────────────────────────────────────────────────────────────┘

100 rows in set. Elapsed: 0.034 sec. Processed 8.87 million rows, 35.50 MB (259.87 million rows/s., 1.04 GB/s.)

MySQL 数据导入

接下来,我们来了解下如何进行数据导入,分别以常见的 MySQL 、 CSV 为例。(官方目前支持几十种数据的直接导入或导出格式:clickhouse.tech/docs/zh/int… ,你可以根据实际需求进行调整,减少不必要数据转换操作。)

启动 MySQL 数据库

为了能够让数据库实例能够互通,我们需要先创建一个虚拟的容器网络:

1
bash复制代码docker network create dbtest

和上文一样,将下面的内容保存为 docker-compose.yml(可以存放另外一个目录,如果存放相同目录,需要调整数据目录名称):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
yaml复制代码version: '3'

services:

mysql:
container_name: mysql
image: mysql:5.7.25
ports:
- 13306:3306
environment:
MYSQL_USER: test
MYSQL_PASSWORD: test
MYSQL_DATABASE: test
MYSQL_ROOT_PASSWORD: test
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --default-storage-engine=INNODB --transaction-isolation=READ-COMMITTED --binlog_format=row --max_allowed_packet=33554432 --sql_mode="STRICT_ALL_TABLES" --local-infile=0
volumes:
# - /etc/localtime:/etc/localtime:ro
# - /etc/timezone:/etc/timezone:ro
- ./data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -p$$MYSQL_ROOT_PASSWORD" ]
networks:
- dbtest

networks:
dbtest:
external: true

接着同样使用 docker-compose up -d 命令,将 MySQL 启动起来,稍后使用。

初始化测试数据

将前文提到的测试数据下载完成后,先进行解压缩,解压缩之后可以看到压缩比差不多 1:6(早些时候的例子是 1:10)。

1
2
3
4
5
bash复制代码unzip yyets_mysql.zip

du -hs yyets*
262M yyets.sql
41M yyets_mysql.zip

将数据扔到数据库容器可以访问到的目录中(推荐使用另外一个方式,方便更直观的对比迁移前后的数据体积):

1
bash复制代码mv yyets-mysql5.7.sql data

或者使用 docker cp 命令,将数据复制进 MySQL 容器的数据文件夹中:

1
bash复制代码docker cp yyets.sql mysql:/var/lib/mysql-files/

接着使用 docker exec 命令登陆容器内的数据终端,先来创建一个空的数据库。

1
2
3
4
5
bash复制代码docker exec -it mysql mysql -uroot -ptest


mysql> create database yyets;
Query OK, 1 row affected (0.01 sec)

接着,将下载好的外部数据源进行初始化,用来 ClickHouse 导入的数据源。

1
2
3
4
5
6
7
bash复制代码docker exec -it mysql bash

# 到底是从 mysql-files 还是从 mysql 目录导入,取决于你将数据放在哪里了
mysql -uroot -ptest yyets < /var/lib/mysql-files/yyets.sql

# 忽略下面的提示
mysql: [Warning] Using a password on the command line interface can be insecure.

稍等片刻数据就都被恢复到 MySQL 实例中了。

导入完毕后,再次使用 du 看看磁盘占用空间,对比导入前的 SQL 文件,磁盘使用空间膨胀了270MB左右(包含索引)。

1
2
bash复制代码du -hs data
530M data

我们再次进入数据库,来看看数据表和数据总量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
sql复制代码docker exec -it mysql mysql -uroot -ptest


# 查看数据库列表
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| test |
| yyets |
+--------------------+
6 rows in set (0.03 sec)


# 切换数据库
mysql> use yyets;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed


#查看数据表
mysql> show tables;
+-----------------+
| Tables_in_yyets |
+-----------------+
| yyets |
+-----------------+
1 row in set (0.00 sec)


# 查看数据量
mysql> select count(*) from yyets;
+----------+
| count(*) |
+----------+
| 17579 |
+----------+
1 row in set (0.83 sec)

可以看到数据库内,只有不到2万条的数据,虽然比较少,但是足够我们用来测试啦。

调整 ClickHouse 实例

为了直观对比数据存储差异,我们可以关闭原来的 ClickHouse 实例后,换一个干净的数据目录,再启动一个新的 ClickHouse 实例。

不过,我们也需要简单调整一下 ClickHouse 配置,让它加入相同的容器网络中:

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码version: "2"

services:

server:
...
networks:
- dbtest

networks:
dbtest:
external: true

过程我就不多赘述了,参考前文即可,不过这里我们先记录一下空的 ClickHouse 目录占用多少磁盘空间。

1
2
3
yaml复制代码clickhouse du -hs *            
2.0M data
4.0K docker-compose.yml

导入 MySQL 数据到 ClickHouse

为了简化导入数据,减少不必要的“数据转换”操作,我们可以通过先创建一个“在线表”的方式来让 CH 自动创建 scheme 。

  • 官方文档中的数据类型:clickhouse.tech/docs/zh/sql…

先使用容器登陆终端 :

1
bash复制代码docker exec -it clickhouse clickhouse-client

接着“挂载” MySQL 到 ClickHouse;

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码981a7a68eb35 :) CREATE DATABASE yyets_online ENGINE = MySQL('mysql:3306', 'yyets', 'root', 'test')

CREATE DATABASE yyets_online
ENGINE = MySQL('mysql:3306', 'yyets', 'root', 'test')

Query id: 45e4570e-3536-4240-9775-fbc9d91ffdcc

Ok.

0 rows in set. Elapsed: 0.026 sec.

981a7a68eb35 :)

切换数据库,查看表数据量,可以看到和 MySQL 中看到的一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码981a7a68eb35 :) use yyets_online;

USE yyets_online

Query id: 9b373b74-11a5-47be-8358-989522311e3e

Ok.

0 rows in set. Elapsed: 0.002 sec.

981a7a68eb35 :) show tables

SHOW TABLES

Query id: 04cf20ce-b656-441e-b414-a63225d498d1

┌─name──┐
│ yyets │
└───────┘

1 rows in set. Elapsed: 0.006 sec.

继续查看表结构,会看到数据类型已经转换好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码981a7a68eb35 :) desc yyets

DESCRIBE TABLE yyets

Query id: 650a4f24-fd8b-4290-b40e-cb59b78a926e

┌─name───────┬─type─────────────┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐
│ id │ Int32 │ │ │ │ │ │
│ url │ Nullable(String) │ │ │ │ │ │
│ name │ Nullable(String) │ │ │ │ │ │
│ expire │ Nullable(Int32) │ │ │ │ │ │
│ expire_cst │ Nullable(String) │ │ │ │ │ │
│ data │ Nullable(String) │ │ │ │ │ │
└────────────┴──────────────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘

6 rows in set. Elapsed: 0.004 sec.

继续“偷懒”,直接使用 show create 语句得到创建当前表结构的命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sql复制代码981a7a68eb35 :) show create yyets

SHOW CREATE TABLE yyets

Query id: 85caee09-9bc0-4e7c-9ff2-bfab820c2c6b

┌─statement──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ CREATE TABLE yyets_online.yyets
(
`id` Nullable(Int32),
`cnname` Nullable(String),
`enname` Nullable(String),
`aliasname` Nullable(String),
`views` Nullable(Int32),
`data` Nullable(String)
)
ENGINE = MySQL('mysql:3306', 'yyets', 'yyets', 'root', 'test') │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

1 rows in set. Elapsed: 0.005 sec.

然后将 数据库引擎部分进行修改,并添加主键即可完成我们后续用于数据计算的离线表的创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
sql复制代码981a7a68eb35 :) CREATE DATABASE yyets_offline;

CREATE DATABASE yyets_offline

Query id: edab3813-f340-4a48-86d7-f12c6ad3297e

Ok.

0 rows in set. Elapsed: 0.019 sec.

981a7a68eb35 :) CREATE TABLE yyets_offline.yyets
:-] (
:-] `id` Int32,
:-] `url` Nullable(String),
:-] `name` Nullable(String),
:-] `expire` Nullable(Int32),
:-] `expire_cst` Nullable(String),
:-] `data` Nullable(String)
:-] ) ENGINE = MergeTree()
:-] ORDER BY `id`

CREATE TABLE yyets_offline.yyets
(
`id` Int32,
`url` Nullable(String),
`name` Nullable(String),
`expire` Nullable(Int32),
`expire_cst` Nullable(String),
`data` Nullable(String)
)
ENGINE = MergeTree
ORDER BY id

Query id: b8371553-683a-418f-9593-b4da49fd5517

Ok.

0 rows in set. Elapsed: 0.031 sec.

981a7a68eb35 :)

最后,使用 insert into 语句将在线表中的数据导入离线表即可。

1
2
3
4
5
6
7
8
9
10
sql复制代码981a7a68eb35 :) insert INTO yyets_offline.yyets select * from yyets_online.yyets

INSERT INTO yyets_offline.resource SELECT *
FROM yyets_online.resource

Query id: fe5eec24-ae0d-4deb-abba-76df242ae8a9

Ok.

0 rows in set. Elapsed: 5.276 sec. Processed 17.58 thousand rows, 244.68 MB (3.33 thousand rows/s., 46.37 MB/s.)

在导入完成后,我们可以先做一个简单的查询,确认数据是否传输完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码SELECT
online,
offline
FROM
(
SELECT count() AS online
FROM yyets_online.yyets
) AS table1
,
(
SELECT count() AS offline
FROM yyets_offline.yyets
) AS table2

Query id: c0edda99-8491-4dd7-bcbc-0323b986553e

┌─online─┬─offline─┐
│ 17579 │ 17579 │
└────────┴─────────┘

1 rows in set. Elapsed: 0.032 sec. Processed 17.52 thousand rows, 74.18 KB (542.94 thousand rows/s., 2.30 MB/s.)

可以看到两个表的数据量一致,代表数据导入就完成了。可以随便试一条简单的关键词匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
sql复制代码466b62ff5dae :) SELECT id, name FROM yyets_offline.`yyets` WHERE `data` LIKE '%漫威%' limit 10

SELECT
id,
name
FROM yyets_offline.yyets
WHERE data LIKE '%漫威%'
LIMIT 10

Query id: 1d1a3884-b73b-4afd-b850-418e8b218e74

┌────id─┬─name─────────────────────┐
│ 29620 │ Marvel Phase Two Preview │
└───────┴──────────────────────────┘
┌────id─┬─name───────────────────────────────┐
│ 30574 │ Marvel One-Shot: Agent Carter │
│ 31794 │ Marvel One-Shot: All Hail The King │
│ 33101 │ Marvel 75 Years: From Pulp to Pop! │
│ 33108 │ Marvel One-Shots │
│ 34471 │ Marvel │
│ 35159 │ Iron Fist │
│ 35506 │ The Defenders │
│ 35565 │ Spider-Man │
│ 35738 │ The Punisher │
└───────┴────────────────────────────────────┘

10 rows in set. Elapsed: 0.277 sec. Processed 17.58 thousand rows, 243.35 MB (63.52 thousand rows/s., 879.26 MB/s.)

数据导入完毕后,记得“打扫卫生”,删除挂载的远程“ online ”数据库的连接即可。

1
2
3
4
5
6
7
8
9
bash复制代码981a7a68eb35 :) drop database yyets_online

DROP DATABASE yyets_online

Query id: 31c82654-a15f-48ba-b27e-e7fa67896ecb

Ok.

0 rows in set. Elapsed: 0.007 sec.

最后,使用 du 查看一下 ClickHouse 使用的磁盘空间吧。

1
2
bash复制代码du -hs data
77M data

相比 MySQL 的 530M 数据存储量来说, ClickHouse 的存储占用真的是非常轻量啦。

CSV 数据导入

一般情况下, CSV 类型数据的导入非常简单,但是 CSV 数据中存在非常多“非标准数据”:比如非严格转义、非严格数值类型标记,混合多系统换行符, field 数据包含“预期之外的数据”等,针对这种情况,我们需要先对数据进行“修复”,因为不同的场景数据完全不同,所以这里没有办法提供一个“万能”的通用方案,不过以过来人的经验,使用 Golang 或者 Node 编写一个清洗工具,执行效率真的蛮快的,感兴趣的同学可以试试看。

我们说回“数据导入”,根据是否还需要 pipeline 处理过程,通常我们有两种方式来进行 CSV 类型的数据导入,如果你的数据都是预先处理过的,可以采取下面的方式来直接导入:

1
bash复制代码clickhouse-client --format_csv_delimiter="|" --format_csv_allow_double_quotes=0 --format_csv_allow_single_quotes=0 --query="INSERT INTO yyets_csv.resource FORMAT CSV" < /var/lib/clickhouse/dump.csv

但是如果你的数据需要使用过程工具来做转换,则可以使用类似下面的方式来导入,过程中可以使用 awk、sed、你自己的工具:

1
bash复制代码cat /var/lib/clickhouse/dump.csv | clickhouse-client --format_csv_delimiter="|" --format_csv_allow_double_quotes=0 --format_csv_allow_single_quotes=0 --query="INSERT INTO yyets_csv.resource FORMAT CSV"

最后

不知不觉写了近万字,先聊到这里吧,后面有时间再聊聊使用过程中踩到的坑。

–EOF


我们有一个小小的折腾群,里面聚集了几百位喜欢折腾的小伙伴。

在不发广告的情况下,我们在里面会一起聊聊软硬件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术沙龙的资料。

喜欢折腾的小伙伴欢迎扫码添加好友。(添加好友,请备注实名,注明来源和目的,否则不会通过审核)

关于折腾群入群的那些事


如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。


本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

本文作者: 苏洋

创建时间: 2021年10月16日
统计字数: 33858字
阅读时间: 68分钟阅读
本文链接: soulteary.com/2021/10/16/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…487488489…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%