4. 数据准备环境:SQL环境搭建与表结构设计

模型A/B测试的数据链路从环境搭建开始。本章将指导你完成MySQL与PostgreSQL的本地/容器化部署、数据库与表结构设计,以及数据安全与权限配置。一个规范的环境是后续SQL实操与结果复现的基础。

4.1 数据库选型:MySQL vs PostgreSQL

在A/B测试场景中,MySQL与PostgreSQL各有优势。下表从关键维度对比两者,帮助你根据团队技术栈与数据规模做出选择。

对比维度 MySQL (8.0+) PostgreSQL (15+)
事务与ACID InnoDB引擎支持,默认提交 原生完整支持,MVCC成熟
窗口函数 8.0后支持,性能稳定 原生支持,函数丰富
CTE递归 8.0后支持 原生支持,性能更优
数组/JSON JSON支持良好,无原生数组 原生JSONB与数组类型,查询灵活
扩展性 主从复制成熟,分库分表方案多 逻辑复制、分区表、扩展插件丰富
A/B测试场景 适合高并发写入、简单聚合 适合复杂统计、多表关联、自定义聚合
选型建议:如果A/B测试数据量在千万级以内,且团队以MySQL为主,使用MySQL 8.0+完全足够。如果涉及复杂的留存分析、用户分群或自定义聚合函数,PostgreSQL是更优选择。

4.2 环境搭建:Docker快速部署

推荐使用Docker进行本地环境搭建,避免污染宿主机,且可一键重置。以下为两种数据库的docker-compose配置。

4.2.1 MySQL 8.0 部署

version: '3.8'
services:
  mysql:
    image: mysql:8.0
    container_name: abtest_mysql
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: abtest_db
      MYSQL_USER: ab_user
      MYSQL_PASSWORD: ab_pass
    ports:
      - "3306:3306"
    volumes:
      - ./mysql_data:/var/lib/mysql
      - ./init_sql:/docker-entrypoint-initdb.d
    command: --default-authentication-plugin=mysql_native_password

4.2.2 PostgreSQL 15 部署

version: '3.8'
services:
  postgres:
    image: postgres:15
    container_name: abtest_pg
    environment:
      POSTGRES_DB: abtest_db
      POSTGRES_USER: ab_user
      POSTGRES_PASSWORD: ab_pass
    ports:
      - "5432:5432"
    volumes:
      - ./pg_data:/var/lib/postgresql/data
      - ./init_sql:/docker-entrypoint-initdb.d
启动命令:在docker-compose.yml所在目录执行 docker-compose up -d,即可后台启动数据库。初始化SQL脚本放在 init_sql 目录下,容器首次启动时会自动执行。

4.3 数据库与表结构设计

模型A/B测试的核心数据模型包含三个主题:实验配置、用户分流、指标事件。以下为通用表结构设计,兼容MySQL与PostgreSQL。

4.3.1 实验配置表 (experiments)

CREATE TABLE experiments (
    experiment_id   VARCHAR(32) PRIMARY KEY,
    experiment_name VARCHAR(128) NOT NULL,
    description     TEXT,
    status          VARCHAR(16) DEFAULT 'running',  -- running, paused, ended
    start_time      TIMESTAMP NOT NULL,
    end_time        TIMESTAMP,
    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

COMMENT ON TABLE experiments IS 'A/B实验配置表';
COMMENT ON COLUMN experiments.status IS '实验状态: running/paused/ended';

4.3.2 实验分组表 (experiment_groups)

CREATE TABLE experiment_groups (
    group_id        VARCHAR(32) PRIMARY KEY,
    experiment_id   VARCHAR(32) NOT NULL REFERENCES experiments(experiment_id),
    group_name      VARCHAR(64) NOT NULL,   -- control / treatment_1 / treatment_2
    group_type      VARCHAR(16) DEFAULT 'treatment',  -- control / treatment
    traffic_ratio   DECIMAL(5,4) NOT NULL,  -- 0.5000 表示50%流量
    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

COMMENT ON TABLE experiment_groups IS '实验分组与流量比例';

4.3.3 用户分流表 (user_assignments)

CREATE TABLE user_assignments (
    id              BIGSERIAL PRIMARY KEY,  -- MySQL使用 BIGINT AUTO_INCREMENT
    user_id         VARCHAR(64) NOT NULL,
    experiment_id   VARCHAR(32) NOT NULL REFERENCES experiments(experiment_id),
    group_id        VARCHAR(32) NOT NULL REFERENCES experiment_groups(group_id),
    assigned_at     TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE (user_id, experiment_id)
);

CREATE INDEX idx_user_exp ON user_assignments(user_id, experiment_id);
COMMENT ON TABLE user_assignments IS '用户分流记录,保证每个实验内用户唯一';

4.3.4 事件表 (events)

CREATE TABLE events (
    event_id        BIGSERIAL PRIMARY KEY,
    user_id         VARCHAR(64) NOT NULL,
    experiment_id   VARCHAR(32) NOT NULL,
    group_id        VARCHAR(32) NOT NULL,
    event_name      VARCHAR(64) NOT NULL,   -- page_view, click, conversion, revenue
    event_value     DECIMAL(12,4),          -- 数值型指标,如金额、时长
    event_time      TIMESTAMP NOT NULL,
    properties      JSONB,                  -- 扩展属性,MySQL使用JSON
    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_event_time ON events(experiment_id, event_time);
CREATE INDEX idx_event_name ON events(experiment_id, event_name);
COMMENT ON TABLE events IS '用户行为事件表,记录所有埋点数据';
注意:PostgreSQL使用 BIGSERIAL 自增类型,MySQL使用 BIGINT AUTO_INCREMENT。JSONB类型在MySQL中对应 JSON。建表时请根据实际数据库调整DDL。

4.4 数据安全与权限管理

A/B测试数据涉及用户ID与行为,必须遵循最小权限原则。以下为权限配置最佳实践。

4.4.1 用户角色划分

角色 权限范围 适用人员
管理员 DDL操作、用户管理、全部表读写 DBA、平台负责人
分析师 实验配置表只读、事件表只读、分流表只读 数据科学家、产品经理
数据工程师 事件表写入、分流表写入、配置表只读 ETL开发、后端开发
只读用户 仅查询已结束实验的聚合视图 业务方、报表系统

4.4.2 MySQL权限示例

-- 创建分析师角色
CREATE USER 'analyst'@'%' IDENTIFIED BY 'analyst_pass';
GRANT SELECT ON abtest_db.experiments TO 'analyst'@'%';
GRANT SELECT ON abtest_db.experiment_groups TO 'analyst'@'%';
GRANT SELECT ON abtest_db.user_assignments TO 'analyst'@'%';
GRANT SELECT ON abtest_db.events TO 'analyst'@'%';
FLUSH PRIVILEGES;

-- 创建数据工程师角色
CREATE USER 'engineer'@'%' IDENTIFIED BY 'engineer_pass';
GRANT SELECT, INSERT, UPDATE ON abtest_db.user_assignments TO 'engineer'@'%';
GRANT SELECT, INSERT ON abtest_db.events TO 'engineer'@'%';
GRANT SELECT ON abtest_db.experiments TO 'engineer'@'%';
FLUSH PRIVILEGES;

4.4.3 PostgreSQL权限示例

-- 创建分析师角色
CREATE ROLE analyst WITH LOGIN PASSWORD 'analyst_pass';
GRANT CONNECT ON DATABASE abtest_db TO analyst;
GRANT USAGE ON SCHEMA public TO analyst;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analyst;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO analyst;

-- 创建数据工程师角色
CREATE ROLE engineer WITH LOGIN PASSWORD 'engineer_pass';
GRANT CONNECT ON DATABASE abtest_db TO engineer;
GRANT USAGE ON SCHEMA public TO engineer;
GRANT SELECT, INSERT, UPDATE ON user_assignments, events TO engineer;
GRANT SELECT ON experiments, experiment_groups TO engineer;
安全建议:生产环境中禁止使用root或postgres超级用户进行日常操作。定期审计权限,移除离职人员账号。事件表中的 user_id 建议使用哈希处理,避免明文存储用户标识。

4.5 环境架构总览

下图展示了从Docker部署到表结构、权限控制的完整数据环境架构。

数据准备环境架构图 数据准备环境架构总览 Docker 容器化部署 MySQL 8.0 (端口3306) PostgreSQL 15 (端口5432) 数据库与表结构 (abtest_db) experiments experiment_groups user_assignments events 主键 · 外键 · 索引 · 唯一约束 · JSONB/JSON 数据安全与权限控制 管理员 (DDL) 分析师 (只读) 数据工程师 (写入) 只读用户 (视图) 💡 环境搭建完成后,使用 docker-compose down -v 可一键重置 所有数据,适合课程练习与实验复现

4.6 初始化数据脚本示例

环境搭建完成后,建议执行以下初始化脚本插入一条实验配置,用于后续SQL实操验证。

-- 插入实验配置
INSERT INTO experiments (experiment_id, experiment_name, description, status, start_time)
VALUES ('exp_001', '首页推荐算法V2', '对比新版推荐算法与旧版在点击率上的差异', 'running', NOW());

-- 插入两个分组:对照组50%,实验组50%
INSERT INTO experiment_groups (group_id, experiment_id, group_name, group_type, traffic_ratio)
VALUES 
('grp_control', 'exp_001', 'control', 'control', 0.5000),
('grp_treatment', 'exp_001', 'treatment_v2', 'treatment', 0.5000);

-- 插入模拟分流数据(10个用户)
INSERT INTO user_assignments (user_id, experiment_id, group_id)
SELECT 
    'user_' || LPAD(CAST(n AS TEXT), 4, '0'),
    'exp_001',
    CASE WHEN n % 2 = 0 THEN 'grp_control' ELSE 'grp_treatment' END
FROM generate_series(1, 10) AS n;
验证环境:执行 SELECT * FROM experiments; 应返回一条记录。执行 SELECT group_id, COUNT(*) FROM user_assignments GROUP BY group_id; 应看到两个分组各约5条记录。