基于 Kafka + Flink + ClickHouse 电商用户行为实时数仓实践

文章目录

需求背景

在电商 SaaS 业务中,平台通常会为商家提供各类营销活动投放能力,例如生成活动链接、推广链接,并通过站内外渠道进行分发。终端用户通过点击这些链接进入活动页面,从而产生访问、浏览等行为数据。

随着业务规模的扩大,平台与商家对活动效果的实时感知能力提出了更高要求,主要体现在以下几个方面:

  • 商家希望能够实时查看活动链接的访问情况,例如当前 PV、UV、DAU 变化趋势
  • 运营人员需要在活动进行过程中,及时判断投放效果是否达标,并快速做出调整
  • 平台侧需要对异常流量、投放效果波动进行实时监控与预警

在传统的数据处理模式中,用户行为数据通常通过离线数仓进行统计分析,数据延迟往往在 T+1 甚至更长。这种方式在日常分析场景下尚可接受,但在电商活动投放这种强时效性场景中已明显无法满足需求:

  • 无法实时感知活动效果
  • 无法支撑活动过程中的快速决策
  • 无法及时发现和定位异常问题

因此,我们需要构建一套低延迟、高吞吐、可扩展的实时数仓体系,对电商活动链接的点击与访问行为进行实时采集与分析,支撑核心指标(如 PV、UV、DAU)的秒级或分钟级统计,为业务运营和商家决策提供实时数据支撑。

基于上述背景,本文设计并实现了一套基于 Kafka + Flink + ClickHouse 的电商活动链接点击行为实时数仓方案,用于对用户访问行为进行实时计算与分析。

整体采用 Kafka 作为实时数据总线、Flink 作为实时计算引擎、ClickHouse 作为实时 OLAP 存储,并按照 ODS → DWD → DWM → DWS → ADS 的分层思想组织数据流转与产出,保证口径稳定、数据可追溯、链路可扩展。

在该体系中,Flink 主要承担 DWD / DWM / DWS 三层的计算职责;ODS 属于"实时采集与缓冲层"(采集端 + Kafka),ADS 属于"面向查询的服务层"(ClickHouse)。这种职责拆分可以有效降低计算引擎与采集、存储系统之间的耦合度。

数仓分层

当用户点击活动链接并进入页面时,会触发前端埋点(或服务端日志)生成行为事件,例如 page_view、click、stay 等。事件通过 SDK/网关统一上报进入 Kafka Topic。Flink 消费 Kafka 流,完成清洗、标准化、去重、聚合,并将明细与指标结果分别写入 ClickHouse,最终由运营后台/商家看板进行查询展示。

ODS:原始数据"原封不动"进来,保留可追溯性(通常在 Kafka)

DWD:清洗、规范化、补全维度字段,形成"干净的明细事实"

DWM:在明细基础上做"轻度加工/中间结果",如会话归因、去重中间态、拉宽

DWS:面向主题的汇总层,产出可复用的指标(按分钟/小时/活动)

ADS:面向应用的最终结果表,直接给 BI/看板/接口用(ClickHouse)

分层 载体 核心职责 产出示例
ODS Kafka 原始事件落地,追溯回放 ods_user_behavior
DWD Kafka 清洗、标准化、统一口径 dwd_user_behavior
DWM Kafka 中间事实/预聚合/去重中间态 dwm_uv_mark / dwm_pv_click_minute
DWS Kafka 主题域指标汇总宽表 dws_activity_kpi_minute
ADS ClickHouse 面向查询/看板的服务层 ads_activity_kpi_minute

数据流简图:

用户埋点

Kafka(日志队列)

Flink(实时计算)

ClickHouse(实时数仓)

BI / 看板 / 实时监控

ODS:原始数据层

ODS 在实时链路里它通常就是"埋点入口 → Kafka 原始 topic。而"埋点入口"在很多电商系统里确实是 Controller/网关接口(或 SDK 直连采集服务)负责接收事件,然后把原始事件写进 Kafka------这一步就可以视为 ODS 写入。

关键原则:ODS 不做复杂清洗,最多做"必要校验 + 保护性补全",其余放到 DWD,伪代码如下:

java 复制代码
@RestController
@RequestMapping("/track")
public class TrackController {
	@Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    private static final String ODS_TOPIC = "ods_user_behavior";

    @PostMapping("/event")
    public String track(@RequestBody TrackEventDTO dto) {
        kafkaTemplate.send(ODS_TOPIC, dto.getUserId(), dto.toString());
        return "ok";
    }
}

DWD:明细层

DWD(Data Warehouse Detail)明细层负责把 ODS 的"原始埋点"加工成"可复用、口径统一的行为明细"。在电商活动场景里,ODS 里的数据往往存在:字段缺失、格式不统一、channel 不规范、userId 为空(匿名用户)等问题。如果让每个下游都各自清洗,不仅重复建设,还容易出现口径不一致。

ODS 由采集服务(Controller)把原始埋点写入 ods_user_behavior;DWD 由 Flink 作业消费 ODS,完成清洗、口径统一与字段标准化,并输出到 dwd_user_behavior 作为后续 DWM/DWS 的统一明细输入。

DWD 的输出写到独立 Kafka Topic:dwd_user_behavior,供 DWM/DWS 多下游复用,伪代码如下:

java 复制代码
public class DwdJobDemo {

    static final ObjectMapper MAPPER = new ObjectMapper();

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(60_000);

        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "kafka:9092");
        props.setProperty("group.id", "flink-dwd-demo");

        // 1) ODS:读 Kafka 原始日志
        FlinkKafkaConsumer<String> source = new FlinkKafkaConsumer<>(
                "ods_user_behavior",
                new SimpleStringSchema(),
                props
        );
        source.setStartFromLatest();

        // 2) DWD:清洗 + 统一口径(输出仍为 JSON 字符串)
        SingleOutputStreamOperator<DwdEvent> dwd = env
                .addSource(source)
                .flatMap((String json, Collector<DwdEvent> out) -> {
                    try {
                        JsonNode n = MAPPER.readTree(json);

                        String event = text(n, "event");
                        String activityId = text(n, "activityId");
                        long ts = n.path("ts").asLong(0);

                        // 极简校验:关键字段必须有
                        if (event == null || activityId == null || ts <= 0) return;
                        if (!("page_view".equals(event) || "click".equals(event))) return;

                        String userId = text(n, "userId");
                        String deviceId = text(n, "deviceId");
                        String uid = (userId != null && !userId.isBlank()) ? userId : deviceId;
                        if (uid == null || uid.isBlank()) return;

                        String channel = text(n, "channel");
                        channel = normalizeChannel(channel);

                        // 产出 DWD 明细
                        DwdEvent e = new DwdEvent();
                        e.event = event;
                        e.activityId = activityId;
                        e.uid = uid;
                        e.channel = channel;
                        e.ts = ts;

                        out.collect(e);
                    } catch (Exception ignored) {}
                })
                .returns(DwdEvent.class)
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<DwdEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                                .withTimestampAssigner((e, t) -> e.ts)
                );

        // 3) 写回 Kafka:dwd_user_behavior
        FlinkKafkaProducer<String> sink = new FlinkKafkaProducer<>(
                "dwd_user_behavior",
                new SimpleStringSchema(),
                props
        );

        dwd
          .map(e -> MAPPER.writeValueAsString(e))
          .addSink(sink);

        env.execute("DWD Demo Job");
    }
}

DWM:中间聚合层

DWM(Data Warehouse Middle)中间层承接 DWD 的"干净明细",做轻量加工,产出可复用的中间事实表。

PV/Click 分钟预聚合

在活动效果分析中,PV/Click 属于最基础、最常用的指标。如果让 DWS 每次都从明细层(DWD)全量扫一遍事件再聚合,会造成重复计算。因此在 DWM 层先做 分钟级预聚合,产出 dwm_pv_click_minute,供 DWS 直接消费汇总,降低计算成本并提升链路清晰度。

伪代码如下:

java 复制代码
public class DwmPvClickJobDemo {

    static final ObjectMapper MAPPER = new ObjectMapper();

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(60_000);

        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "kafka:9092");
        props.setProperty("group.id", "flink-dwm-pvclick-demo");

        // 1) 读 DWD 明细
        FlinkKafkaConsumer<String> source = new FlinkKafkaConsumer<>(
                "dwd_user_behavior",
                new SimpleStringSchema(),
                props
        );
        source.setStartFromLatest();

        SingleOutputStreamOperator<DwdEvent> dwd = env
                .addSource(source)
                .map(json -> MAPPER.readValue(json, DwdEvent.class))
                .returns(DwdEvent.class)
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<DwdEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                                .withTimestampAssigner((e, t) -> e.ts)
                );

        // 2) 分钟窗口聚合:PV/Click
        SingleOutputStreamOperator<PvClickMinute> pvClickMinute = dwd
                .keyBy(e -> e.activityId + "|" + e.channel)
                .window(TumblingEventTimeWindows.of(Time.minutes(1)))
                .process(new ProcessWindowFunction<DwdEvent, PvClickMinute, String, TimeWindow>() {
                    @Override
                    public void process(String key, Context ctx, Iterable<DwdEvent> elements, Collector<PvClickMinute> out) {
                        long pv = 0, click = 0;
                        for (DwdEvent e : elements) {
                            if ("page_view".equals(e.event)) pv++;
                            else if ("click".equals(e.event)) click++;
                        }
                        String[] p = key.split("\\|");
                        PvClickMinute r = new PvClickMinute();
                        r.activityId = p[0];
                        r.channel = p[1];
                        r.minuteStart = ctx.window().getStart();
                        r.pv = pv;
                        r.click = click;
                        out.collect(r);
                    }
                })
                .name("DWM-PV-CLICK-MINUTE");

        // 3) 写 Kafka:dwm_pv_click_minute
        FlinkKafkaProducer<String> sink = new FlinkKafkaProducer<>(
                "dwm_pv_click_minute",
                new SimpleStringSchema(),
                props
        );

        pvClickMinute
                .map(x -> MAPPER.writeValueAsString(x))
                .addSink(sink)
                .name("SINK-DWM-PVCLICK");

        env.execute("DWM PV/Click Minute Demo Job");
    }
}

UV 去重中间态

UV 是最典型的中间层产物:如果直接在 DWS/ADS 里做 count distinct(uid),流式计算会带来较大的状态压力,且乱序/迟到处理更复杂。因此我们把 UV 的"去重中间态"下沉到 DWM:在同一分钟内,同一 uid 在同一 activityId + channel 维度只输出一次,形成 UV=1 的标记数据流。

java 复制代码
public class DwmUvJobDemo {

    static final ObjectMapper MAPPER = new ObjectMapper();

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(60_000);

        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "kafka:9092");
        props.setProperty("group.id", "flink-dwm-demo");

        // 1) 读 DWD 明细
        FlinkKafkaConsumer<String> source = new FlinkKafkaConsumer<>(
                "dwd_user_behavior",
                new SimpleStringSchema(),
                props
        );
        source.setStartFromLatest();

        SingleOutputStreamOperator<DwdEvent> dwd = env
                .addSource(source)
                .map(json -> MAPPER.readValue(json, DwdEvent.class))
                .returns(DwdEvent.class)
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<DwdEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                                .withTimestampAssigner((e, t) -> e.ts)
                );

        // 2) DWM:一分钟内对 uid 去重,输出 UV mark
        SingleOutputStreamOperator<UvMark> uvMark = dwd
                .filter(e -> "page_view".equals(e.event)) // UV 口径:按浏览统计
                .keyBy(e -> {
                    long minuteStart = (e.ts / 60_000) * 60_000;
                    // key = 活动 + 渠道 + 分钟 + 用户
                    return e.activityId + "|" + e.channel + "|" + minuteStart + "|" + e.uid;
                })
                .process(new MinuteUvDedup())
                .name("DWM-UV-MARK");

        // 3) 写 Kafka:dwm_uv_mark
        FlinkKafkaProducer<String> sink = new FlinkKafkaProducer<>(
                "dwm_uv_mark",
                new SimpleStringSchema(),
                props
        );

        uvMark.map(m -> MAPPER.writeValueAsString(m)).addSink(sink);

        env.execute("DWM UV Demo Job");
    }

    // ---------- 去重算子:同一分钟同 uid 只输出一次 ----------
    public static class MinuteUvDedup extends KeyedProcessFunction<String, DwdEvent, UvMark> {
        private transient ValueState<Boolean> seen;

        @Override
        public void open(org.apache.flink.configuration.Configuration parameters) {
            seen = getRuntimeContext().getState(new ValueStateDescriptor<>("seen", Boolean.class));
        }

        @Override
        public void processElement(DwdEvent e, Context ctx, Collector<UvMark> out) throws Exception {
            if (Boolean.TRUE.equals(seen.value())) return;

            seen.update(true);

            long minuteStart = (e.ts / 60_000) * 60_000;
            long minuteEnd = minuteStart + 60_000;

            out.collect(new UvMark(e.activityId, e.channel, minuteStart, minuteEnd, e.uid));

            // 窗口结束后清理状态,防止状态无限增长
            ctx.timerService().registerEventTimeTimer(minuteEnd + 1);
        }

        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<UvMark> out) throws Exception {
            seen.clear();
        }
    }
}

DWS:汇总层

DWS 以 DWM 的中间结果为唯一输入,做主题域指标汇总与拼接:DWS 以 DWM 的中间结果为唯一输入,不再直接处理明细或做复杂去重,而是对已标准化的中间事实进行主题域指标汇总与拼接:

从 dwm_pv_click_minute 获取分钟 PV/Click

从 dwm_uv_mark 聚合得到分钟 UV

最终合并为 dws_activity_kpi_minute(activityId, channel, minuteStart, pv, uv,click),下游 ADS 可直接写 ClickHouse 或供看板查询。

伪代码如下:

java 复制代码
public class DwsFromDwmJobDemo {

    static final ObjectMapper MAPPER = new ObjectMapper();

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(60_000);

        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "kafka:9092");
        props.setProperty("group.id", "flink-dws-from-dwm-demo");

        // 1) 读 DWM:分钟 PV/Click
        FlinkKafkaConsumer<String> pvClickSource =
                new FlinkKafkaConsumer<>("dwm_pv_click_minute", new SimpleStringSchema(), props);
        pvClickSource.setStartFromLatest();

        SingleOutputStreamOperator<PvClickMinute> pvClick = env
                .addSource(pvClickSource)
                .map(json -> MAPPER.readValue(json, PvClickMinute.class))
                .returns(PvClickMinute.class)
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<PvClickMinute>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                                .withTimestampAssigner((e, t) -> e.minuteStart)
                );

        // 2) 读 DWM:UV mark(需要在 DWS 内聚合成 UV)
        FlinkKafkaConsumer<String> uvMarkSource =
                new FlinkKafkaConsumer<>("dwm_uv_mark", new SimpleStringSchema(), props);
        uvMarkSource.setStartFromLatest();

        SingleOutputStreamOperator<UvMark> uvMark = env
                .addSource(uvMarkSource)
                .map(json -> MAPPER.readValue(json, UvMark.class))
                .returns(UvMark.class)
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<UvMark>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                                .withTimestampAssigner((e, t) -> e.minuteStart)
                );

        // 3) UV mark -> 分钟 UV 聚合(count)
        SingleOutputStreamOperator<UvMinute> uvMinute = uvMark
                .keyBy(m -> m.activityId + "|" + m.channel)
                .window(TumblingEventTimeWindows.of(Time.minutes(1)))
                .process(new ProcessWindowFunction<UvMark, UvMinute, String, TimeWindow>() {
                    @Override
                    public void process(String key, Context ctx, Iterable<UvMark> elements, Collector<UvMinute> out) {
                        long uv = 0;
                        for (UvMark ignored : elements) uv++;
                        String[] p = key.split("\\|");
                        UvMinute r = new UvMinute();
                        r.activityId = p[0];
                        r.channel = p[1];
                        r.minuteStart = ctx.window().getStart();
                        r.uv = uv;
                        out.collect(r);
                    }
                });

        // 4) 把 pvClick 和 uvMinute 合并成 DWS 宽表(按 activityId+channel+minuteStart)
        SingleOutputStreamOperator<DwsKpiMinute> dws = pvClick
                .map(p -> Partial.fromPvClick(p)).returns(Partial.class)
                .union(uvMinute.map(u -> Partial.fromUv(u)).returns(Partial.class))
                .keyBy(x -> x.activityId + "|" + x.channel + "|" + x.minuteStart)
                .process(new MergeWide())
                .name("DWS-MERGE");

        // 5) 输出 DWS topic
        FlinkKafkaProducer<String> sink =
                new FlinkKafkaProducer<>("dws_activity_kpi_minute", new SimpleStringSchema(), props);

        dws.map(x -> MAPPER.writeValueAsString(x)).addSink(sink);

        env.execute("DWS From DWM Demo Job");
    }

    // -------- 合并:部分字段 -> 宽表 --------
    public static class MergeWide extends KeyedProcessFunction<String, Partial, DwsKpiMinute> {
        private transient ValueState<DwsKpiMinute> state;

        @Override
        public void open(org.apache.flink.configuration.Configuration parameters) {
            state = getRuntimeContext().getState(new ValueStateDescriptor<>("wide", DwsKpiMinute.class));
        }

        @Override
        public void processElement(Partial in, Context ctx, Collector<DwsKpiMinute> out) throws Exception {
            DwsKpiMinute cur = state.value();
            if (cur == null) {
                cur = new DwsKpiMinute();
                cur.activityId = in.activityId;
                cur.channel = in.channel;
                cur.minuteStart = in.minuteStart;
            }
            cur.pv += in.pv;
            cur.click += in.click;
            cur.uv += in.uv;

            state.update(cur);

            // 分钟结束后输出
            long fireTs = in.minuteStart + 60_000 + 2_000;
            ctx.timerService().registerEventTimeTimer(fireTs);
        }

        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<DwsKpiMinute> out) throws Exception {
            DwsKpiMinute cur = state.value();
            if (cur != null) {
                out.collect(cur);
                state.clear();
            }
        }
    }
}

ADS:应用数据服务层

ADS(Application Data Service)应用层是面向业务消费的最终数据层,强调"可用、稳定、高性能"。在实时数仓中,ADS 一般承接 DWS 的主题指标,将其落到面向分析查询的存储(如 ClickHouse),并根据查询场景设计分区与排序键,保证大促期间高并发下仍能秒级响应。

CK 表结构大致如下:

sql 复制代码
CREATE TABLE IF NOT EXISTS ads_activity_kpi_minute (
  activity_id String,
  channel String,
  minute_start DateTime,
  pv UInt64,
  uv UInt64,
  click UInt64
)
ENGINE = MergeTree
PARTITION BY toDate(minute_start)
ORDER BY (activity_id, channel, minute_start);

伪代码如下:

java 复制代码
public class AdsToClickHouseDemo {

    static final ObjectMapper MAPPER = new ObjectMapper();

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(60_000);

        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "kafka:9092");
        props.setProperty("group.id", "flink-ads-demo");

        // 1) 读 DWS 指标
        FlinkKafkaConsumer<String> source =
                new FlinkKafkaConsumer<>("dws_activity_kpi_minute", new SimpleStringSchema(), props);
        source.setStartFromLatest();

        var dws = env.addSource(source)
                .map(json -> MAPPER.readValue(json, DwsKpiMinute.class))
                .returns(DwsKpiMinute.class)
                // 这层其实不需要严格 watermark,随便加一个 demo 用
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<DwsKpiMinute>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                                .withTimestampAssigner((e, t) -> e.minuteStart)
                );

        // 2) ClickHouse JDBC Sink(插入 ADS 表)
        var ckSink = JdbcSink.sink(
                "INSERT INTO ads_activity_kpi_minute(activity_id, channel, minute_start, pv, uv, click) VALUES(?,?,?,?,?,?)",
                (PreparedStatement ps, DwsKpiMinute k) -> {
                    ps.setString(1, k.activityId);
                    ps.setString(2, k.channel);
                    ps.setTimestamp(3, new java.sql.Timestamp(k.minuteStart));
                    ps.setLong(4, k.pv);
                    ps.setLong(5, k.uv);
                    ps.setLong(6, k.click);
                },
                JdbcExecutionOptions.builder()
                        .withBatchSize(1000)
                        .withBatchIntervalMs(1000)
                        .withMaxRetries(3)
                        .build(),
                new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
                        .withUrl("jdbc:clickhouse://clickhouse:8123/default")
                        .withDriverName("com.clickhouse.jdbc.ClickHouseDriver")
                        .build()
        );

        dws.addSink(ckSink).name("ADS-CLICKHOUSE");

        env.execute("ADS to ClickHouse Demo");
    }

    // DWS 输出结构(和你 DWS 一致)
    public static class DwsKpiMinute {
        public String activityId;
        public String channel;
        public long minuteStart;
        public long pv;
        public long uv;
        public long click;
    }
}

DWS 输出的主题指标是"计算结果",ADS 是"可查询的服务层";通过 ClickHouse 的 MergeTree(分区 + 排序)把实时指标固化下来,支撑大促看板的秒级查询与高并发读取。

总结

层级 核心问题 主要作用 是否做计算
ODS 发生了什么? 原始落地、可追溯
DWD 这是什么行为? 清洗、统一口径 ⚠️ 轻
DWM 能不能提前算? 中间事实、预聚合
DWS 指标是多少? 主题指标汇总
ADS 怎么给人用? 查询 / 服务 / 看板
  • ODS:Controller/SDK → Kafka(ods_user_behavior)
  • DWD:ods_user_behavior → 清洗统一 → Kafka(dwd_user_behavior)
  • DWM:
    • dwd_user_behavior → UV 去重 → Kafka(dwm_uv_mark)
    • dwd_user_behavior → PV/Click 预聚合 → Kafka(dwm_pv_click_minute)
  • DWS:只读 DWM → 合并输出 → Kafka(dws_activity_kpi_minute)
  • ADS:dws_activity_kpi_minute → ClickHouse(ads_activity_kpi_minute)
相关推荐
春日见6 小时前
拉取与合并:如何让个人分支既包含你昨天的修改,也包含 develop 最新更新
大数据·人工智能·深度学习·elasticsearch·搜索引擎
你这个代码我看不懂7 小时前
@RefreshScope刷新Kafka实例
分布式·kafka·linq
Elastic 中国社区官方博客8 小时前
如何防御你的 RAG 系统免受上下文投毒攻击
大数据·运维·人工智能·elasticsearch·搜索引擎·ai·全文检索
YangYang9YangYan9 小时前
2026中专大数据与会计专业数据分析发展路径
大数据·数据挖掘·数据分析
W1333090890710 小时前
工业大数据方向,CDA证书和工业数据工程师证哪个更实用?
大数据
Re.不晚10 小时前
可视化大数据——淘宝母婴购物数据【含详细代码】
大数据·阿里云·云计算
Elastic 中国社区官方博客11 小时前
Elasticsearch:交易搜索 - AI Agent builder
大数据·人工智能·elasticsearch·搜索引擎·ai·全文检索
SQL必知必会11 小时前
使用 SQL 进行 RFM 客户细分分析
大数据·数据库·sql
YangYang9YangYan11 小时前
2026大专大数据技术专业学数据分析指南
大数据·数据挖掘·数据分析