Cat Blog
HomeBlogTools
Search
Language
Choose site style
Choose accent color
Click Effect
Theme

Cat Blog · Updated regularly. Source code is available on GitHub.

Superset + Trino 跨数据源联合查询方案

March 10th, 2026
测试

Series

生活记录技术文章

Series

生活记录

Progress 2 / 2

生活记录

Previous in series

深度解析:高性能微服务网关的架构演进与落地实践

Next in series

This is the last post in this series.

1. 查询需求分析

1.1 原始查询逻辑

MemberProfileServiceImpl#getUserIdsByCondition 方法实现了以下查询逻辑:

TEXT
多组条件 → 取并集 (UNION)
├── 条件组1 → 用户表条件 ∩ 会员状态条件 (INTERSECT)
├── 条件组2 → 用户表条件 ∩ 会员状态条件 (INTERSECT)
└── ...

每组条件包含:

条件类型字段数据源
地区条件regionType + regionIdszhl_member_profile (birth_province_code/city_code/area_code) 或 zhl_member_scopes
年级条件userGradezhl_member_profile.grade
注册时间registerStartTime + registerEndTimezhl_members.create_time
手机号phoneNumberszhl_members.phone
会员状态memberStatussby_user_member (memberType + beginTime + afterExpirationTime)

会员状态说明:

状态值说明SQL条件
1当前为非会员用户不在会员表中 或 会员已过期
2当前为VIP会员member_type=1 AND after_expiration_time > now
3当前为SVIP会员member_type=2 AND after_expiration_time > now
4当前为SVIP+会员member_type=3 AND after_expiration_time > now
5会员3天内到期now < after_expiration_time <= now + 3天
6会员7天内到期now < after_expiration_time <= now + 7天
7会员1个月内到期now < after_expiration_time <= now + 30天
8会员过期3天内now - 3天 < after_expiration_time <= now
9会员过期7天内now - 7天 < after_expiration_time <= now
10会员过期1个月内now - 30天 < after_expiration_time <= now

1.2 数据源结构

用户中心库 (zhl_ucenter_mysql)

TXT
zhl_members              # 用户基础信息表
├── id                   # 用户ID (uid)
├── phone                # 手机号
├── create_time          # 注册时间
└── regist_success       # 注册成功标识 (1=成功)

zhl_member_profile       # 用户详细信息表
├── uid                  # 用户ID
├── grade                # 年级
├── birth_province_code  # 省份编码
├── birth_city_code      # 城市编码
└── birth_area_code      # 地区编码

zhl_member_book_mapping  # 用户业务绑定表
├── uid                  # 用户ID
└── business_id          # 业务ID

zhl_member_scopes        # 用户范围表(学校)
├── uid                  # 用户ID
└── scope_id             # 范围ID

会员库 (zhl_recharge_mysql)

TXT
sby_user_member          # 用户会员信息表
├── uid                  # 用户ID
├── product_id           # 产品ID
├── member_type          # 会员类型 (1=VIP, 2=SVIP, 3=SVIP+)
├── begin_time           # 生效开始时间
└── after_expiration_time # 到期时间

2. Trino 数据源配置

2.1 Trino Catalog 配置

在 Trino 的 etc/catalog/ 目录下配置 MySQL 数据源:

zhl_ucenter_mysql.properties (用户中心库)

PROPERTIES
connector.name=mysql
connection-url=jdbc:mysql://user-db-host:3306/zhl_ucenter
connection-user=trino_user
connection-password=trino_password

zhl_recharge_mysql.properties (会员库)

PROPERTIES
connector.name=mysql
connection-url=jdbc:mysql://member-db-host:3306/zhl_recharge
connection-user=trino_user
connection-password=trino_password

2.2 Superset 数据库配置

在 Superset 中添加 Trino 数据源:

  1. 进入 Settings > Database Connections
  2. 点击 + Database 添加新数据库
  3. 选择 Trino 作为数据库类型
  4. 配置连接参数:
YAML
SQLAlchemy URI: trino://user:password@trino-host:8080/hive
  1. 记录生成的 Database ID(后续API调用需要)

3. Superset Dataset 配置(Jinja 模板 SQL)

3.1 创建 Virtual Dataset

在 Superset 中创建虚拟数据集,使用 Jinja 模板语法 实现动态 SQL 拼接。

Dataset 名称: user_condition_query Dataset 类型: Virtual Dataset (SQL)

完整 SQL(保存到 Dataset):

SQL
-- ================================================
-- 用户条件查询 - Trino 跨数据源联合查询(优化版)
-- ================================================
-- 参数说明:
--   business_id: 业务ID(必填)
--   product_id: 产品ID(必填)
--   region_type: 区域类型 (1-省份 2-城市 3-地区 4-学校)
--   region_ids: 地区ID列表,逗号分隔(Jinja 自动加引号)
--   grades: 年级列表,逗号分隔
--   register_start_time: 注册开始时间(Unix时间戳)
--   register_end_time: 注册结束时间(Unix时间戳)
--   phone_numbers: 手机号列表,逗号分隔(Jinja 自动加引号)
--   member_statuses: 会员状态列表,逗号分隔
--   page_no: 页码(从1开始)
--   page_size: 每页数量

{% set business_id = business_id or 1 %}
{% set product_id = product_id or 1 %}
{% set region_type = region_type or 1 %}
{% set region_ids = region_ids or '350000' %}
{% set grades = grades or '1,2' %}
{% set register_start_time = register_start_time or 0 %}
{% set register_end_time = register_end_time or 0 %}
{% set phone_numbers = phone_numbers or '18285664817' %}
{% set member_statuses = member_statuses or '1,2' %}
{% set page_no = page_no or 1 %}
{% set page_size = page_size or 100 %}


{# 将逗号分隔的字符串转换为带引号的 SQL IN 格式 #}
{% macro quote_list(value) %}
    {% if value and value != '' %}
        {% set items = value.split(',') %}
        {% for item in items %}
            '{{ item.strip() }}'
            {% if not loop.last %},{% endif %}
        {% endfor %}
    {% endif %}
{% endmacro %}


SELECT DISTINCT m.id AS uid
FROM test_sby_ucenter.sby_ucenter.zhl_member_book_mapping mbm
        INNER JOIN test_sby_ucenter.sby_ucenter.zhl_members m ON mbm.uid = m.id
        LEFT JOIN test_sby_ucenter.sby_ucenter.zhl_member_profile mp ON m.id = mp.uid
        LEFT JOIN test_sby_recharge_center.sby_recharge_center.sby_user_member um
                  ON m.id = um.uid
                     AND um.product_id = {{ product_id }}
   AND um.begin_time <= to_unixtime(now())
WHERE mbm.business_id = {{ business_id }}
  AND m.regist_success = 1
    {# 地区条件 #}
    {% if region_type > 0 and region_ids != '' %}
  AND (
    {% if region_type == 1 %}
   mp.birth_province_code IN ({{ quote_list(region_ids) }})
    {% elif region_type == 2 %}
   mp.birth_city_code IN ({{ quote_list(region_ids) }})
    {% elif region_type == 3 %}
   mp.birth_area_code IN ({{ quote_list(region_ids) }})
    {% elif region_type == 4 %}
   mp.school_id IN ({{ region_ids }})
    {% endif %}
   )
    {% endif %}
    {# 年级条件 #}
    {% if grades != '' %}
  AND mp.grade IN ({{ grades }})
    {% endif %}
    {# 注册时间条件 #}
    {% if register_start_time > 0 %}
  AND m.create_time >= {{ register_start_time }}
    {% endif %}
    {% if register_end_time > 0 %}
  AND m.create_time <= {{ register_end_time }}
    {% endif %}
    {# 手机号条件 #}
    {% if phone_numbers != '' %}
  AND m.phone IN ({{ quote_list(phone_numbers) }})
    {% endif %}
    {# 会员状态条件 #}
    {% if member_statuses != '' %}
    {% set statuses = member_statuses.split(',') %}
   AND (
   {# 非会员条件(与其他条件是 OR 关系)#}
        {% if '1' in statuses %}
        (um.after_expiration_time IS NULL OR um.after_expiration_time <= to_unixtime(now()))
            {% if statuses|length > 1 %}
            OR
            {% endif %}
        {% endif %}
        {# 会员类型条件: 2=VIP, 3=SVIP, 4=SVIP+ #}
        {% if '2' in statuses or '3' in statuses or '4' in statuses %}
        (
            um.after_expiration_time > to_unixtime(now())
            AND um.member_type IN (
                {% if '2' in statuses %}1{% endif %}
                {% if '3' in statuses %}{% if '2' in statuses %},{% endif %}2{% endif %}
                {% if '4' in statuses %}{% if '2' in statuses or '3' in statuses %},{% endif %}3{% endif %}
            )
        )
            {% if '5' in statuses or '6' in statuses or '7' in statuses or '8' in statuses or '9' in statuses or '10' in statuses %}
            OR
            {% endif %}
        {% endif %}
        {# 即将到期条件: 5=3天, 6=7天, 7=30天 #}
        {% if '5' in statuses %}
        (um.after_expiration_time > to_unixtime(now()) AND um.after_expiration_time <= to_unixtime(now()) + 259200)
            {% if '6' in statuses or '7' in statuses or '8' in statuses or '9' in statuses or '10' in statuses %}
            OR
            {% endif %}
        {% endif %}
        {% if '6' in statuses %}
        (um.after_expiration_time > to_unixtime(now()) AND um.after_expiration_time <= to_unixtime(now()) + 604800)
            {% if '7' in statuses or '8' in statuses or '9' in statuses or '10' in statuses %}
            OR
            {% endif %}
        {% endif %}
        {% if '7' in statuses %}
        (um.after_expiration_time > to_unixtime(now()) AND um.after_expiration_time <= to_unixtime(now()) + 2592000)
            {% if '8' in statuses or '9' in statuses or '10' in statuses %}
            OR
            {% endif %}
        {% endif %}
        {# 已过期条件: 8=3天, 9=7天, 10=30天 #}
        {% if '8' in statuses %}
        (um.after_expiration_time <= to_unixtime(now()) AND um.after_expiration_time > to_unixtime(now()) - 259200)
            {% if '9' in statuses or '10' in statuses %}
            OR
            {% endif %}
        {% endif %}
        {% if '9' in statuses %}
        (um.after_expiration_time <= to_unixtime(now()) AND um.after_expiration_time > to_unixtime(now()) - 604800)
            {% if '10' in statuses %}
            OR
            {% endif %}
        {% endif %}
        {% if '10' in statuses %}
        (um.after_expiration_time <= to_unixtime(now()) AND um.after_expiration_time > to_unixtime(now()) - 2592000)
        {% endif %}
    )
    {% endif %}
ORDER BY m.id
   LIMIT {{ page_size }} OFFSET {{ (page_no - 1) * page_size }}

优化说明:

  • 将原来的 5 个 CTE 子查询合并为单次 LEFT JOIN 查询
  • 用户表与会员表直接关联,避免多次扫描
  • 会员状态条件在 WHERE 子句中直接判断,减少中间结果集

3.2 Dataset 参数说明

参数名类型必填说明
business_idInteger是业务ID
product_idInteger是产品ID(会员查询用)
region_typeInteger否区域类型: 1-省份 2-城市 3-地区 4-学校
region_idsString否地区ID列表,逗号分隔,如 "110000,120000"
gradesString否年级列表,逗号分隔,如 "1,2,3"
register_start_timeLong否注册开始时间(Unix时间戳)
register_end_timeLong否注册结束时间(Unix时间戳)
phone_numbersString否手机号列表,逗号分隔,如 "13800138000,13800138001"
member_statusesString否会员状态列表,逗号分隔,如 "2,3,5"
page_noInteger否页码(从1开始),默认1
page_sizeInteger否每页数量,默认100

3.3 Jinja 模板语法说明

Superset 支持的 Jinja 模板语法:

语法说明示例
{{ variable }}变量输出{{ business_id }}
{% if condition %}...{% endif %}条件判断见上方 SQL
{% set var = value %}变量定义{% set page_size = params.page_size or 100 %}
{% for item in list %}...{% endfor %}循环用于列表处理
{% macro name(args) %}...{% endmacro %}宏定义{% macro quote_list(value) %}...{% endmacro %}
{{ params.xxx }}获取 API 传入参数{{ params.business_id }}
{%- ... -%}去除空白{%- if condition -%}...{%- endif -%}
value.split(',')字符串分割{% set items = region_ids.split(',') %}
item.strip()去除空白'{{ item.strip() }}'

5. Superset API 调用实现

5.1 API 接口说明

使用 SQL Lab API 执行 Dataset SQL

  • 接口路径: /api/v1/sqllab/execute
  • 请求方法: POST
  • 认证方式: Bearer Token
  • 优势: 支持 template_params 参数,可传入 Jinja 模板变量

注意: Chart Data API (/api/v1/chart/data) 不支持 params 参数传递 Jinja 模板变量,需使用 SQL Lab API。

请求示例:

JSON
{
  "database_id": 1,
  "sql": "-- 从 Dataset 复制或预定义的 SQL 模板",
  "template_params": "{\"business_id\":1,\"product_id\":1,\"member_statuses\":\"2,3\"}",
  "limit": 100
}

注意: template_params 参数类型为 string,需要将 JSON 对象序列化为字符串传递。

Table of Contents

Current section:1. 查询需求分析

  • 1. 1. 查询需求分析
  • 2. 1.1 原始查询逻辑
  • 3. 1.2 数据源结构
  • 4. 2. Trino 数据源配置
  • 5. 2.1 Trino Catalog 配置
  • 6. 2.2 Superset 数据库配置
  • 7. 3. Superset Dataset 配置(Jinja 模板 SQL)
  • 8. 3.1 创建 Virtual Dataset
  • 9. 3.2 Dataset 参数说明
  • 10. 3.3 Jinja 模板语法说明
  • 11. 5. Superset API 调用实现
  • 12. 5.1 API 接口说明
Back to top

Related Posts

View all posts
Index 21: Terminal UX —— Claude Code 风格终端交互

Index 21: Terminal UX —— Claude Code 风格终端交互

August 21st, 2026

Index 21 将 cat-code 终端交互升级为 Claude Code 风格,解决旧 REPL 黑箱、无中断及输入体验差的问题。通过 JLine3 与 Mordant 实现历史补全、实时工具可见性、Spinner 状态及 Esc 中断。核心采用 UI 与 AgentLoop 解耦的事件流架构,支持内联权限菜单与优雅降级。同时配置 logback 收敛控制台日志,确保 TUI 清爽且功能无损,显著提升可用性。

Index 20: Comprehensive Agent —— 全机制集成(收口)

Index 20: Comprehensive Agent —— 全机制集成(收口)

August 13th, 2026

作为收官之作,把 s01-s19 的二十个核心机制整合为一个全面智能体:统一的状态查询与运行周期、自治认领与工作区隔离的贯通,以及 /status 命令的全局可视化。全机制协同运转,标志着 Cat-Code 从零完整复刻 Claude Code 核心能力的收官。

Index 19: MCP Plugin —— 多传输 / 通道路由 / 工具池组装

Index 19: MCP Plugin —— 多传输 / 通道路由 / 工具池组装

August 13th, 2026

引入 MCP(Model Context Protocol)插件机制,通过多传输适配与通道路由,把外部 MCP 服务器的工具动态接入智能体的工具池。工具注册从静态编译期扩展为运行时动态组装,让智能体能力随外部服务即插即用。