在广告系统中,展示功能对数据读取的实时性和写入的吞吐量都有较高要求。如果初期把广告内容、投放规则、展示记录全部堆进一两张表里,随着业务增长,查询会变得越来越慢,统计也会因为锁竞争出现偏差。合理拆分实体并控制单表字段,是设计高效MySQL表结构的第一步。

核心实体与表拆分思路
广告展示功能至少涉及三类数据:广告本身的内容与状态、投放计划与定向条件、以及每次实际展示产生的日志。把它们放在同一张表里,会导致写日志时频繁更新广告行,引发行锁争用。更合理的做法是将广告主数据、投放数据、曝光数据彻底分离。
广告表只保存素材与基础状态,投放表保存时间段、地域、人群等定向信息并冗余广告标题等常用字段,曝光表则只记录关联ID、时间、设备号。这样展示接口查询时只需读投放表和广告表的少量字段,写曝光表完全不影响广告主体的读取。
广告表结构示例
下面给出广告表的基础定义,状态字段用tinyint方便索引,素材地址单独存储避免大字段拖慢查询。
CREATE TABLE ad_info ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, advertiser_id BIGINT UNSIGNED NOT NULL, title VARCHAR(64) NOT NULL, material_url VARCHAR(255) NOT NULL, status TINYINT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
投放与曝光表设计
投放表通过advertise_id关联广告,同时冗余title减少联表。曝光表按天分表,只保留必要字段,用唯一索引防止同一设备短时间重复曝光。
CREATE TABLE ad_delivery ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, ad_id BIGINT UNSIGNED NOT NULL, title VARCHAR(64) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, region_code VARCHAR(16) NOT NULL, PRIMARY KEY (id), KEY idx_ad (ad_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE ad_show_log_20240101 ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, delivery_id BIGINT UNSIGNED NOT NULL, device_id VARCHAR(64) NOT NULL, show_time DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_dev_time (device_id, show_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
展示查询的优化策略
展示接口最忌讳实时join多张表做过滤。更稳妥的办法是在应用层先用Redis缓存符合条件的投放ID集合,MySQL只根据ID快速取广告内容。这样数据库压力被前置缓存分担,且表结构不需要为复杂查询建很多联合索引。
另一个常见误区是把曝光统计实时写回广告表计数。正确方式是用曝光表异步汇总,或者通过消息队列落盘后批量更新。表结构上曝光日志独立,也方便后期用分区表或归档表清理旧数据,不影响主表性能。
使用预查询减少联表
下面的代码演示了如何从缓存获取候选投放ID,再回表取广告内容,避免大范围扫描。
<?php
// 假设已从Redis拿到候选投放ID数组
$candidateIds = $redis->smembers('ad:candidates:region_1');
if (empty($candidateIds)) {
return [];
}
$placeholders = implode(',', array_fill(0, count($candidateIds), '?'));
$sql = "SELECT a.title, a.material_url FROM ad_info a
JOIN ad_delivery d ON a.id = d.ad_id
WHERE d.id IN ($placeholders) AND a.status = 1";
$stmt = $pdo->prepare($sql);
$stmt->execute($candidateIds);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
索引与字段类型注意点
广告展示场景的查询通常按状态和关联ID过滤,因此status、ad_id这类字段必须建索引。但索引不是越多越好,写入频繁的曝光表应尽量减少二级索引,仅靠主键和唯一约束防重即可。
字段类型上,时间用DATETIME而非字符串,地域码用定长VARCHAR或SMALLINT映射,设备号若长度固定可考虑CHAR。避免在大表上使用TEXT或BLOB,素材地址等长文本应控制长度并放到独立字段。
| 表名 | 推荐索引 | 避免操作 |
|---|---|---|
| ad_info | 主键、status索引 | 存大文本素材 |
| ad_delivery | ad_id索引 | 实时统计展示数 |
| ad_show_log | 唯一设备时间约束 | 建过多二级索引 |
分表与归档实践
曝光日志随展示量线性增长,单表迟早遇到瓶颈。按天或按月分表后,历史表可迁移到低成本存储。应用层通过时间路由到对应表,对业务透明。
归档时直接RENAME TABLE为备份名,再新建空表,比DELETE大量数据更安全。广告与投放表体量小,配合软删除字段即可,不需要频繁物理清理。
表结构服务于访问模式,先理清展示与统计的读写比例,再决定冗余与拆分,比盲目套用范式更重要。