列式数据库ClickHouse学习总结

[toc]

ClickHouse

介绍

ClickHouse是一种开源的列式数据库管理系统(DBMS),专门设计用于大规模数据分析。它以高性能和扩展性而闻名,并且被广泛应用于各种数据分析和实时查询场景。

以下是ClickHouse的一些主要特点和优势:

  1. 列式存储:ClickHouse使用列式存储,将数据按列进行存储,而不是按行。这种存储方式在大规模数据分析中具有优势,因为它可以压缩数据、提升查询性能、减少IO操作等。
  2. 高性能:ClickHouse被设计为高性能的数据分析引擎。它通过并行处理和多核优化等技术,能够快速地执行各种复杂的查询操作。ClickHouse还支持数据分区和复制,以进一步提高查询的性能和可用性。
  3. 扩展性:ClickHouse可以轻松地横向扩展,以处理大规模的数据集和高并发的查询请求。它支持分布式架构和自动数据分片,可以将数据和查询负载分散到多个节点上,从而实现水平扩展。
  4. 实时查询:尽管ClickHouse主要用于数据分析,但它也能够支持实时查询。它提供了一种称为“近似精确度”的技术,可以在轻微的精度损失下快速计算结果,从而实现快速的实时查询。
  5. SQL兼容性:ClickHouse支持标准SQL查询语言,使得用户可以使用熟悉的SQL语句来查询和分析数据。同时,ClickHouse还支持其他一些高级功能和扩展,例如窗口函数、数据聚合和数据转换等。

总的来说,ClickHouse是一个功能丰富的列式数据库系统,适用于大规模的数据分析和实时查询。它具有高性能、高扩展性和SQL兼容性等优势,被广泛应用于数据仓库、日志分析、实时报表等领域。

使用场景

ClickHouse的使用场景可以包括以下几个方面:

  1. 数据仓库:ClickHouse适用于构建数据仓库,用于存储和分析大规模的结构化和半结构化数据。它可以处理数十亿、甚至数万亿行的数据,支持复杂的查询和聚合操作。
  2. 实时分析:ClickHouse可以处理实时数据分析场景,包括实时查询、实时报表和仪表板等。它能够快速地处理大量的数据,并在几秒或几毫秒内返回查询结果。
  3. 日志分析:ClickHouse在处理日志分析方面表现出色。它可以处理海量的日志数据,并支持快速的搜索、过滤和聚合操作。这使得它成为实时监控和故障排查的理想选择。
  4. 时间序列数据分析:由于列式存储和高性能查询的支持,ClickHouse非常适用于处理大规模的时间序列数据。它可以用于监测系统、物联网设备、金融数据等领域。
  5. 分布式查询处理:ClickHouse支持分布式查询和数据复制,可以在多个节点之间分散查询负载,并提高查询的性能和可用性。这使得它可以应对大规模的并发查询和高吞吐量的应用场景。

总的来说,ClickHouse适用于需要处理大规模数据和高并发查询的数据分析场景。它在数据仓库、实时分析、日志分析和时间序列数据分析等方面具有广泛的应用。

存储引擎

ClickHouse使用了自己独特的列式存储引擎,称为MergeTree存储引擎,它是为高性能分析而设计的。

MergeTree存储引擎基于列式存储的原则。它将表中的每一列分别存储在独立的文件中,每个文件包含该列的所有值。这种存储方式使得ClickHouse在处理大规模数据时表现卓越,因为它可以只读取和处理需要的列,节省了对其他列的读取和解析。

MergeTree存储引擎支持对数据进行分区和排序。数据可以按照时间范围进行分区,每个分区内的数据按照一个或多个列进行排序。这些特性使得ClickHouse在处理时间序列数据时非常高效,可以快速地查询和分析特定时间段的数据。

此外,MergeTree存储引擎还支持数据压缩和多级索引。数据压缩可以减少存储空间的占用,并提高磁盘IO性能。多级索引可以加速数据的查找和过滤,提高查询的效率。

总的来说,MergeTree存储引擎是ClickHouse的核心组件,它采用了列式存储、分区、排序、数据压缩和多级索引等技术,使得ClickHouse在处理大规模数据和高性能查询方面具有优势。

安装(docker)

直接使用命令:

docker run -d --name clickhouse-server -p 8123:8123 -p 9000:9000 clickhouse/clickhouse-server:22.8.14.53

如果需要做数据持久化:

以下是在使用Docker安装ClickHouse时实现持久化的步骤:

  1. 创建数据目录:在主机上创建一个用于存储ClickHouse数据的目录。例如,您可以创建一个名为/path/to/clickhouse/data的目录。
  2. 运行容器时挂载数据目录:在运行ClickHouse容器时,使用-v参数来将主机上的数据目录挂载到容器内。运行以下命令来创建并启动ClickHouse容器,并挂载数据目录:
docker run -d --name clickhouse-server \
-p 8123:8123 -p 9000:9000 \
-v /path/to/clickhouse/data:/var/lib/clickhouse \
clickhouse/clickhouse-server:22.8.14.53

此命令将主机上的/path/to/clickhouse/data目录挂载到容器内的/var/lib/clickhouse目录上。这样可以确保容器内的数据持久化保存在主机上的指定位置。

  1. 验证持久化:等待容器启动后,可以使用以下命令验证数据是否持久化保存到了指定目录:
ls /path/to/clickhouse/data

如果你能看到ClickHouse数据文件和目录的列表,这意味着数据成功地持久保存在了主机上的数据目录中。

通过以上步骤,您已经成功地使用Docker安装并配置了持久化的ClickHouse实例。这将确保您的数据在容器重启或迁移时不会丢失。

简单实例

建表

当使用ClickHouse进行SQL操作时,首先需要创建一个用户信息的表。以下是一个示例的SQL语句,用于创建一个名为users的表,包含用户的姓名、年龄和电子邮件:

CREATE TABLE users (
    name String,
    age Int32,
    email Nullable(String)
) ENGINE = MergeTree()
ORDER BY name;

在该表的创建语句中,name列使用String数据类型存储用户姓名,age列使用Int32数据类型存储用户年龄,email列使用Nullable(String)数据类型存储用户电子邮件,允许为空值。表的引擎使用了MergeTree引擎,并按name列进行排序。

完成表的创建后,我们可以使用SQL命令对users表进行数据操作。以下是一些常见的操作示例:

插入数据

INSERT INTO users (name, age, email)
VALUES ('Alice', 25, 'alice@example.com');

INSERT INTO users (name, age)
VALUES ('Bob', 30);

查询数据

SELECT * FROM users;

SELECT name, age FROM users WHERE age > 25;

更新数据

ALTER TABLE users UPDATE age = age + 1 WHERE name = 'Alice';

删除数据

ALTER TABLE users DELETE WHERE name = 'Bob';

复杂表结构

定义表结构

CREATE TABLE IF NOT EXISTS pcp_product_view (
    event_id String,   //字符串
    name String,
    event_src String,
    created_at DateTime, //时间
    data Tuple (   //元组,内嵌
        user_id Int64,   //int类型
        product_id String,
        domain String,
        subdomain String,
        store_id Int64,
        retailer_id Int64,
    ),
    geoip Tuple (
        ip String,
        country String
    ),
    device Tuple (
        browser String,
        version String,
        os String
    ),
    headers Tuple (
        ev String,
        site String,
    ),
    retailer Tuple (
        id Int64,
        domain String,
    ),
    client Tuple (
        id String,
        web_id String,
        url String,
        timestamp_ms Int64
    ),
    store Tuple (
        id Int64,
        domain String,
    ),
    extension Tuple (
        id String,
        session_id String,
        serial_id Int64,
        version String
    ),
    tab Tuple (
        id Int64,
        session_id String,
        serial_id Int64,
        page_language String
    )
) engine = MergeTree PARTITION BY toYYYYMMDD(toDateTime(created_at, 'America/Los_Angeles'))
        ORDER BY created_at
        SETTINGS index_granularity = 8192;

添加字段

ALTER TABLE pcp_product_view ADD COLUMN user Tuple(id Int8) AFTER data;

GO操作CH

安装依赖

go get github.com/ClickHouse/clickhouse-go/v2

要使用Github上的clickhouse-go库来连接到ClickHouse,你需要做以下准备工作:

  1. 在Go项目中导入clickhouse-go库:
goCopy code
import (
    "github.com/ClickHouse/clickhouse-go/v2"
)
  1. 使用clickhouse-go库来建立到ClickHouse数据库的连接。你需要提供ClickHouse数据库的连接信息和凭据。以下是一个简单的示例:
func main() {
    // 配置ClickHouse连接信息
    dsn := "tcp://localhost:9000?database=your_database&username=default&password=your_password"

    // 建立到ClickHouse的连接
    conn, err := clickhouse.OpenConnection(dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // 在这里可以执行ClickHouse查询和操作
    // 例如:conn.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS example_table (id Int32, name String)")

    // 更多操作和查询的示例可以在clickhouse-go库的文档中找到
}

在上面的示例中,我们通过clickhouse.OpenConnection(dsn)建立到ClickHouse数据库的连接,并通过defer conn.Close()在使用完连接后关闭连接。

请确保将示例中的your_database替换为你实际的数据库名称,your_password替换为你实际的密码。

这样就可以来操作clickhouse了。

该包提供了以下常用方法用于数据的增删改查操作:

  1. Exec(query string, args …interface{}) error: 执行一个查询语句,无需返回结果。
  2. Select(query string, args …interface{}) (sql.Rows, error): 执行SELECT查询,并返回查询结果的sql.Rows对象。
  3. SelectRow(query string, args …interface{}) sql.Row: 执行SELECT查询,并返回查询结果的sql.Row对象。
  4. Insert(table string, data map[string]interface{}) error: 插入数据到指定的表中,data是一个包含列名和对应值的map。
  5. CopyFromReader(table string, reader io.Reader) (int, error): 从一个io.Reader读取数据,并将数据复制到指定的表中。
  6. ExecContext(ctx context.Context, query string, args …interface{}) (sql.Result, error): 在指定的上下文中执行一个查询语句,无需返回结果。
  7. InsertContext(ctx context.Context, table string, data map[string]interface{}) (sql.Result, error): 在指定的上下文中将数据插入到指定的表中。
  8. SelectContext(ctx context.Context, query string, args …interface{}) (sql.Rows, error): 在指定的上下文中执行SELECT查询,并返回查询结果的sql.Rows对象。
  9. SelectRowContext(ctx context.Context, query string, args …interface{}) sql.Row: 在指定的上下文中执行SELECT查询,并返回查询结果的sql.Row对象。
  10. Begin() (*sql.Tx, error): 开始一个数据库事务。
  11. WithTimeout(timeout time.Duration): 设置数据库操作的超时时间。

以上是一些常用的方法,用于在Go中进行ClickHouse数据库的增删改查操作。具体的使用方法和参数可以根据实际需求进行参考。

实例

func LogToCH(event string, req EventRequest) error {
    //获取到CH的连接
    CH, err := config.GetClickhouseConn(config.ClickHouse)
    if err != nil {
        log.Println("获取CH客户端失败:", err)
        return err
    }
    defer CH.Close()

    eventCh := ReqToEventCH(event, req)
    query := fmt.Sprintf("INSERT INTO %s (event_id, name, event_src,created_at, data, user, geoip, device, headers, retailer, client, store, extension, tab) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)", event)
    //执行插入操作
    stmt, err := CH.PrepareBatch(context.Background(), query)
    if err != nil {
        log.Println("插入预操作失败:", err)
        return err
    }
    for _, v := range eventCh {
        err = stmt.Append(v["event_id"], v["name"], v["event_src"], time.Now(), v["data"], v["user"], v["geoip"], v["device"], v["headers"], v["retailer"], v["client"], v["store"], v["extension"], v["tab"])
        if err != nil {
            log.Println("写入数据到CH失败:", err)
            return err
        }
    }
    err = stmt.Send()
    if err != nil {
        log.Println("发送失败:", err)
        return err
    }
    return nil
}

ch对数据结构要求非常的严格,必须数据类型和字段完全匹配

func ReqToEventCH(name string, req EventRequest) []map[string]interface{} {
    resp := make([]map[string]interface{}, 0)
    for _, v := range req.Events {
        data := make(map[string]interface{})

        if name == event001 {
            b, err := json.Marshal(v.Data)
            if err != nil {
                log.Println("转json失败")
                return nil
            }
            product := DataProductView{}
            err = json.Unmarshal(b, &product)
            if err != nil {
                log.Println("解json失败")
                return nil
            }

      //不同的data内部结构不同,需要判断
            data = productView(product)

    } else if{
      ……
    }
    events := map[string]interface{}{
            "event_id":   v.ID,
            "name":       name,
            "event_src":  v.EventSrc,
            "created_at": time.Now(),
            "data":       data,
            "user": map[string]interface{}{
                "id": int8(v.User.ID),
            },
            "geoip": map[string]interface{}{
                "ip":      v.Geoip.Ip,
                "country": v.Geoip.Country,
            },
            "retailer": map[string]interface{}{
                "id":         int64(v.Retailer.ID),
                "domain":     v.Retailer.Domain,
            },
            "client": map[string]interface{}{
                "id":           v.Client.ID,
                "url":          v.Client.URL,
                "web_id":       "",
                "timestamp_ms": int64(v.Client.TimestampMS),
            },
            "headers": map[string]interface{}{
                "ev":   req.EventHeaders.Header.Ev,
                "site": req.EventHeaders.Header.Site,
                "bl":   req.EventHeaders.Header.Bl,
            },
            "extension": map[string]interface{}{
                "id":         v.Extension.ID,
                "session_id": v.Extension.SessionID,
                "serial_id":  int64(v.Extension.SerialId),
                "version":    v.Extension.Version,
            },
            "device": map[string]interface{}{
                "browser": v.Device.Browser,
                "version": v.Device.Version,
                "os":      v.Device.Os,
            },
            "store": map[string]interface{}{
                "id":         int64(v.Store.ID),
                "domain":     v.Store.Domain,
                "subdomain":  v.Store.Subdomain,
            },
            "tab": map[string]interface{}{
                "id":            int64(v.Tab.ID),
                "session_id":    v.Tab.SessionID,
            },
        }
        resp = append(resp, events)
    }
    return resp
}

func productView(d DataProductView) map[string]interface{} {
    return map[string]interface{}{
        "user_id":         int64(d.UserId),
        "product_id":      d.ProductId,
        "domain":          d.Domain,
        "subdomain":       d.Subdomain,
        "store_id":        int64(d.StoreId),
        "retailer_id":     int64(d.RetailerId),
    }
}

总结

对应clickhouse的简单学习和使用我们了解这里就差不多了,当然还有更多的关于性能优化,索引等等这里就不介绍,对于使用CH其实都是使用SQL已经进行操作,所以SQL是很重要的,好吧本文也没什么值得学习的,就简单的介绍和使用了CH希望能加深你对它的了解。

本作品采用《CC 协议》,转载必须注明作者和本文链接
刻意学习
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
118
粉丝
89
喜欢
173
收藏
246
排名:365
访问:2.6 万
私信
所有博文
社区赞助商