跳到主要内容

分表、分区与查询优化

先确定要查的业务事实和日期,再选择数据库、表或索引,最后使用匹配的字段条件。查询分区表尽量携带分区字段,查询时序表必须设置时间范围。

按业务选择数据​

要查什么数据对象首先限定的范围
设备档案及大批量筛选MySQL nebula_ids.base_device、ES base_deviceMAC+CPU,或明确渠道、型号和激活日期;ES 是检索副本,可能有同步延迟
某设备某月装了什么MySQL ik_app_install.app_install_deviceYYYYMM、ES 同名月索引先选月,再选设备;每月最近快照不能还原任意历史时点
某应用装在多少设备ik_app_install.app_install_count_package月份、包名、版本;离线汇总有更新时间差
应用每天运行多久、打开几次TDengine nebula_ids/app_runtime_archive.flow_app_runtime冷热库、运行日期、包名及设备;不是 ES 安装快照
APP 活跃趋势TDengine app_activity_day/month/year.app_activity_detail周期粒度、包名、周期起点
应用活跃原始上报MySQL ik_thirdparty.app_activity_log_*应用、服务端接收日、设备分区值
设备日志文件ik_yudao_device.device_log_fileMAC、CPUID、创建时间;查询元数据,不读取所有日志正文
黑名单卸载或杀进程反馈MySQL flow_blacklisted_device、TDengine app_kill_record策略、设备、业务日期或接收时间;两种时间不要混用
广告播放和执行结果TDengine launcher_ad_play*、launcher_exec时间、包名、广告位置/素材、反馈类型;播放次数不等于去重设备数

MySQL:先选表,再选分区和索引​

按月或按日分表决定访问哪些表;LIST(partition_index) 决定表内设备数据分布;联合索引决定如何定位候选记录。查询时先限定日期表,再尽量携带分区字段与联合索引的前导列。按设备哈希分区的 LIST 表,需要显式提供 partition_index。MySQL 分区裁剪说明

对象实际组织方式使用时注意
安装快照月表(202606—202609)每表 150 个 LIST(partition_index),值 1—150先选月份,再使用与写入一致的设备分区值;MAC/CPU 条件不自动变成 partition_index
APP 活跃原始日表(25 张)每表 1024 个 LIST(partition_index),值 0—1023先按应用与接收日选表;哈希计算得到分区值,不表示物理方式为 HASH
rule_mac_item、rule_mac_resource_item每表 300 个 LIST(partition_index)使用规则或资源包 ID、MAC 与分区值;不能拿安装表的分区算法通用代替
device_log_file32 个 KEY(mac,cpuid)同时提供设备两个标识;同时限定create_time范围
APK 推送历史主表及两个旁支表每表 16 个 KEY(mac,cpuid)设备维度分区,同时限定推送时间;两个旁支表保留为未接入的历史结构
遗留 app_install_device/app_install_package分别 150/100 个 LIST 分区与独立安装库现行月表区分,不直接跨库合并统计

37张表采用分区设计,分区字段和取值范围见对应库页的“分区设计”。

分区字段如何计算​

查询分区表时,尽量带上分区字段。下列三组 LIST 分区由业务计算 partition_index 后写入;查询同一设备时必须使用相同的输入格式和算法。先按日期选表,再把分区值和设备条件一起放入 WHERE。

表族分表规则partition_index 计算分区名
安装快照 app_install_deviceYYYYMM服务端接收月份,后缀不带下划线MAC+CPU 的 Java 字符串哈希,对150取余后取绝对值,再加1p1—p150
活跃原始日志 app_activity_log_包名_YYYYMMDD包名中的点替换为下划线,日期取服务端接收日非空白MAC+非空白CPU的 Java 字符串哈希,对1024取余后取绝对值P0—P1023
rule_mac_item固定表规则ID哈希分成10组,MAC哈希分成30组,组合成分区值p1010—p1039、p1110—p1139,依次至p1910—p1939
rule_mac_resource_item固定表与上一行相同,ID改为资源包ID同上

MAC 与 CPU 直接拼接,不加分隔符;分区计算不做 MAC 去冒号、大小写转换或修剪。查询应使用保存时的设备标识。安装快照把空白 CPU 视为空串;活跃日志分别跳过空白 MAC 和 CPU。规则表使用完整 MAC,ID采用 Java Long.hashCode,不能把ID转成字符串后计算。

Java​

以下方法放在工具类中使用。StrUtil 按 Hutool 5.8.35 的空白字符规则处理输入。

import cn.hutool.core.util.StrUtil;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.stream.IntStream;

static int installPartition(String mac, String cpu) {
String key = mac + (StrUtil.isBlank(cpu) ? "" : cpu);
return Math.abs(key.hashCode() % 150) + 1;
}

static int activityPartition(String mac, String cpu) {
String key = (StrUtil.isBlank(mac) ? "" : mac)
+ (StrUtil.isBlank(cpu) ? "" : cpu);
return Math.abs(key.hashCode() % 1024);
}

static String installTable(LocalDate receivedDay) {
return "app_install_device" + receivedDay.format(DateTimeFormatter.ofPattern("yyyyMM"));
}

static String activityTable(String packageName, LocalDate receivedDay) {
return "app_activity_log_" + packageName.replace(".", "_") + "_"
+ receivedDay.format(DateTimeFormatter.BASIC_ISO_DATE);
}

static int rulePartition(long ruleOrResourceId, String mac) {
int high = Math.abs(Long.hashCode(ruleOrResourceId) % 10) + 10;
int low = Math.abs(mac.hashCode() % 30) + 10;
return high * 100 + low;
}

// 只知道规则ID或资源包ID时,查询这30个分区。
static int[] rulePartitionsById(long id) {
int high = Math.abs(Long.hashCode(id) % 10) + 10;
return IntStream.range(10, 40).map(low -> high * 100 + low).toArray();
}

// 只知道MAC时,反查其所属规则/资源包,查询这10个分区。
static int[] rulePartitionsByMac(String mac) {
int low = Math.abs(mac.hashCode() % 30) + 10;
return IntStream.range(10, 20).map(high -> high * 100 + low).toArray();
}

Python​

Python 内置 hash() 与 Java 字符串哈希不同;负数取余的规则也不同。以下 Python 3.10+ 示例保留 Java 的 UTF-16 字符单元、32位溢出和余数符号,得到相同的分区值。日期参数采用与写入端一致的业务时区。

from datetime import date

BLANK_CHARS = (
set(range(0x0009, 0x000E)) | set(range(0x001C, 0x0020))
| set(range(0x2000, 0x200B))
| {0x0020, 0x00A0, 0x1680, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000,
0xFEFF, 0x202A, 0x0000, 0x3164, 0x2800, 0x180E}
)

def blank(value: str | None) -> bool:
return value is None or all(ord(c) in BLANK_CHARS for c in value)

def signed32(value: int) -> int:
value &= 0xFFFFFFFF
return value if value < 0x80000000 else value - 0x100000000

def java_hash(value: str) -> int:
raw = value.encode("utf-16-be", errors="surrogatepass")
h = 0
for i in range(0, len(raw), 2):
h = signed32(31 * h + (raw[i] << 8) + raw[i + 1])
return h

def java_long_hash(value: int) -> int:
value &= 0xFFFFFFFFFFFFFFFF
return signed32(value ^ (value >> 32))

def java_remainder(value: int, divisor: int) -> int:
result = abs(value) % divisor
return -result if value < 0 else result

def install_partition(mac: str, cpu: str | None) -> int:
key = mac + ("" if blank(cpu) else cpu)
return abs(java_remainder(java_hash(key), 150)) + 1

def activity_partition(mac: str | None, cpu: str | None) -> int:
key = ("" if blank(mac) else mac) + ("" if blank(cpu) else cpu)
return abs(java_remainder(java_hash(key), 1024))

def install_table(received_day: date) -> str:
return "app_install_device" + received_day.strftime("%Y%m")

def activity_table(package_name: str, received_day: date) -> str:
return "app_activity_log_" + package_name.replace(".", "_") + "_" + received_day.strftime("%Y%m%d")

def rule_partition(rule_or_resource_id: int, mac: str) -> int:
high = abs(java_remainder(java_long_hash(rule_or_resource_id), 10)) + 10
low = abs(java_remainder(java_hash(mac), 30)) + 10
return high * 100 + low

def rule_partitions_by_id(value: int) -> list[int]:
high = abs(java_remainder(java_long_hash(value), 10)) + 10
return [high * 100 + low for low in range(10, 40)]

def rule_partitions_by_mac(mac: str) -> list[int]:
low = abs(java_remainder(java_hash(mac), 30)) + 10
return [high * 100 + low for high in range(10, 20)]

把分区值放入查询条件​

查询某月某设备的安装快照,先计算150分区值,再同时提供 mac、cpu_id 和 partition_index,对应联合唯一索引 mac_cpu(mac,cpu_id,partition_index):

SELECT mac, cpu_id, apps
FROM ik_app_install.app_install_device202609
WHERE mac = :mac AND cpu_id <=> :cpu AND partition_index = :partition_index;

CPU查询参数保留原存储值。分区算法把空白CPU用于哈希时视为空串,不会改变表中保存的CPU;<=> 同时支持 NULL 与非NULL值匹配。

查规则内的某台设备,使用ID+MAC计算的单个分区值;只按规则ID导出设备时,使用该ID对应的30个分区值,避免遍历全部300个分区。资源包明细同理,将 liteflow_chain_id 换成 mac_resource_id。

SELECT mac
FROM ik_rule.rule_mac_item
WHERE liteflow_chain_id = :rule_id
AND partition_index IN (:p1, :p2, :p3 /* 展开完整30个分区值 */);

活跃原始日志先确定包名和服务端接收日,再带 partition_index、MAC、CPU及所需时间条件。日期跨天时逐日选择目标表,不能把设备上报时间直接当成接收日选表。

活跃日志分区读取还使用 PARTITION(pN) 显式选择分区,N 为上述1024分区算法的结果。例如计算结果为42时,查询目标日表的 PARTITION(p42)。表名和分区名由固定规则生成;设备值和时间值使用参数绑定。

KEY 分区:由 MySQL 按设备字段分配​

device_log_file 使用 KEY(mac,cpuid) 32分区,apk_push_history 使用同一分区字段的16分区。查询尽量同时提供 mac、cpuid,并限定业务时间范围。KEY 的哈希由 MySQL 计算,应用传入设备字段即可。MySQL KEY 分区说明

设备日志查询使用 idx_mac_cpuid_create_time(mac,cpuid,create_time):

String sql = "SELECT id, create_time FROM device_log_file "
+ "WHERE mac = ? AND cpuid = ? AND create_time >= ? AND create_time < ?";
try (var statement = connection.prepareStatement(sql)) {
statement.setString(1, mac);
statement.setString(2, cpu);
statement.setTimestamp(3, fromTime);
statement.setTimestamp(4, toTime);
try (var rows = statement.executeQuery()) {
while (rows.next()) {
long id = rows.getLong("id");
// 处理当前范围内的日志文件元数据。
}
}
}
sql = (
"SELECT id, create_time FROM device_log_file "
"WHERE mac = %s AND cpuid = %s AND create_time >= %s AND create_time < %s"
)
cursor.execute(sql, (mac, cpu, from_time, to_time))

安装包明细的包名分区规则​

安装统计任务还定义 app_install_packageYYYYMM 月表,按包名分成100个 LIST 分区。当前库表清单中的独立安装库未列出该月表族;单体遗留库的 app_install_package 为历史结构,查询历史数据时以其保存的 partition_index 为准。

月表规则先查下面的固定包名映射;未命中时计算 abs(Java String.hashCode(packageName) % 59) + 1。包名保留大小写和原始字符。负值分区名为 p0 加绝对值,例如 -1 对应 p01;正值为 p 加数值。查询已生成的月表时,包名条件与分区值一起提供。

包名与固定分区值清单(100项)
包名分区值分区名
com.android.chrome-1p01
com.google.android.youtube.tv-2p02
com.valor.mfc.droid.tvapp.generic-3p03
com.mm.droid.livetv.tve-4p04
com.netflix.mediaclient-5p05
com.global.unitviptv-6p06
com.droidlogic.mboxlauncher-7p07
com.simple.appstore.flymarket-8p08
com.world.youcinetv-9p09
org.xbmc.kodi-10p010
com.ionitech.airscreen-11p011
com.android.calculator2-12p012
com.android.rockchip-13p013
com.rockchips.mediacenter-14p014
com.rockchip.wfd-15p015
com.android.apkinstaller-16p016
com.unitvnet.tvod-17p017
org.chromium.webview_shell-18p018
com.nathnetwork.xciptv-19p019
com.ktcp.osvideo-20p020
com.cloudinfinitegroup.skit-21p021
com.droidlogic.mediacenter-22p022
com.droidlogic.mboxlauncher.rk-23p023
com.iron.allapp-24p024
com.netflix.ninja-25p025
com.home.cast.dlna.renderer-26p026
com.mm.droid.livetv.tvees-27p027
com.google.android.googlequicksearchbox-28p028
com.google.android.videos-29p029
com.amazon.avod.thirdpartyclient-30p030
com.esaba.downloader-31p031
com.android.mgstv-32p032
com.cloudinfinitegroup.xinfoapp-33p033
com.google.android.youtube-34p034
com.cloudinfinitegroup.xgamesapp-35p035
com.charon.rocketfly-36p036
com.facebook.katana-37p037
link.ntdev.ntdw-38p038
com.okshopping.video-39p039
com.disney.disneyplus-40p040
com.youku.intl.tv-41p041
com.twitter.android1p1
com.spotify.tv.android2p2
com.skype.raider3p3
com.uv.droid.launcher.mxqlauncher4p4
com.mm.droid.livetv.bluetv5p5
com.tiktok.tv6p6
tv.pluto.android7p7
cm.aptoidetv.pt8p8
com.android.mgandroid9p9
com.spotify.music10p10
cm.aptoide.pt11p11
com.yablio.sendfilestotv12p12
com.android.tv13p13
com.mxtech.videoplayer.ad14p14
com.integration.unitviptv15p15
com.hbo.hbonow16p16
com.bladetv.android17p17
com.mm.droid.livetv.redplaybox18p18
com.nst.iptvsmarterstvbox19p19
com.google.android.youtube.tvkids20p20
com.devcoder.iptvxtreamplayer21p21
com.deep.cast.dlna.renderer22p22
com.bbqbar.browser23p23
com.globo.globotv24p24
com.cbs.ca25p25
com.dots.color.connect.puzzle.neon.line26p26
com.digitalseva.iptvplayer27p27
com.ak.allapp28p28
com.plexapp.android29p29
org.videolan.vlc30p30
com.mm.droid.livetv.wakatv31p31
com.iptvBlinkPlayer32p32
com.banglalink.toffeetv33p33
tv.twitch.android.app34p34
com.whatsapp35p35
com.rmdigital.tvs6236p36
com.sea.movie.ad37p37
com.google.android.gm38p38
io.wareztv.android.one39p39
com.nst.smartersplayer40p40
com.blade.phx541p41
com.wbd.stream42p42
com.google.android.apps.youtube.kids43p43
com.alphainventor.filemanager44p44
com.cetusplay.remoteservice45p45
com.tvs.phx546p46
com.newbraz.p2p47p47
zank.remote48p48
com.droidlogic.mboxlauncher.wetv49p49
com.p2elite.brtv250p50
com.batanga.vixtv51p51
com.teamsmart.videomanager.tv52p52
com.beesp2p.phx5.lite53p53
com.bongo.bioscope54p54
com.divergentftb.xtreamplayeranddownloader55p55
com.instantbits.cast.receiver56p56
updata.com.ik.updataversion57p57
com.instagram.android58p58
com.centralp2p.plus59p59

Java:fixedPartitions 按上表初始化;取值优先于哈希回退。

static int packagePartition(String packageName, java.util.Map<String, Integer> fixedPartitions) {
Integer fixed = fixedPartitions.get(packageName);
return fixed != null ? fixed : Math.abs(packageName.hashCode() % 59) + 1;
}

static String packagePartitionName(int partition) {
return partition < 0 ? "p0" + Math.abs(partition) : "p" + partition;
}

Python:复用前面的 java_hash、java_remainder,fixed_partitions 按上表初始化。

def package_partition(package_name: str, fixed_partitions: dict[str, int]) -> int:
if package_name in fixed_partitions:
return fixed_partitions[package_name]
return abs(java_remainder(java_hash(package_name), 59)) + 1

def package_partition_name(partition: int) -> str:
return "p0" + str(abs(partition)) if partition < 0 else "p" + str(partition)

MySQL:具体查询对应哪个索引​

查询场景实际索引及列顺序边界
精确找设备base_device.mac_cpu(mac,cpu),唯一单 MAC 不一定唯一;渠道、型号、激活时间另外建有单独索引
某月某设备的安装快照mac_cpu(mac,cpu_id,partition_index),唯一先筛设备再展开 apps;apps 未定义包名 JSON 路径索引
设备日志时间线idx_mac_cpuid_create_time(mac,cpuid,create_time)MAC、CPUID 等值后限制时间;只按时间另有 idx_create_time(create_time)
某任务的推送成功记录flow_task_device.idx_ftd_task_id_push_success(task_id,is_push_success)“推送成功”不等于安装运行成功
某设备某任务的记录flow_task_device.mac(mac,task_id),唯一设备条件在前,与按任务聚合是不同查询方向
应用运行排行榜stats_app_runtime_top.key(date,package_name,type,version_code),唯一;另有 date(date)只有日期+类型不能当作前两列连续定位;包名包含匹配不能当等值
报表数据源编码uk_tenant_code(tenant_id,code),唯一不含 deleted,逻辑删除不会释放编码
黑名单卸载明细blacklisted_id(blacklisted_id,mac,cpu,event_id,device_time),唯一create_time 不在索引内;末尾可空字段也影响去重语义
APK 推送历史uk_mac_cpuid_package_version(mac,cpuid,package_name,version_code_key,push_task_time_key),唯一用两个生成列归一空值;主表没有 PRIMARY,不把名为 id 的唯一索引写成主键

联合索引查询优先使用左侧连续列,等值条件之后再安排范围条件。新增索引应结合查询频率、扫描范围和写入成本。MySQL 多列索引说明

原始活跃日表仅 PRIMARY(id,partition_index)。即使已选对日表和分区,分区内按 MAC、CPU、接收时间筛选排序仍缺少对应二级索引。Task 独立库的 8 张表、BPM 的 9 张业务表也均只有主键,非主键筛选应限制查询日期与结果范围,大数据量场景应配置匹配业务条件的索引。

TDengine:查询必须限定时间范围​

对象首时间列主要标签范围与业务口径
flow_app_runtimerecord_timepackage_name、day设备上报的运行时间;当月和上月查热库,更早查归档;跨冷热边界拆开查询,日期差不超过两个月
app_activity_detailtspackage_name、record_time日/月/年分别选库;月首、年首对齐。当前业务窗口:日最多30天、月730天、年3650天
device_activity_detailtsrecord_time、region_id区分上报来源和周期,同设备跨来源不能直接累加为去重设备数
device_runtime_detailstart_timemac、cpu查设备运行区间,考虑补报;不是接收时间范围
launcher_ad_play_detailbegin_time包名、版本、广告位置、周期及分类等明确业务时区和埋点含义,不把所有位置状态都当播放成功
launcher_ad_play_device_countrecord_time包名、版本、位置、time_tag、分类time_tag 日/月/年分别为 yyyyMMdd/yyyyMM/yyyy;选汇总粒度后再查询
launcher_exec_recordtsad_resource_id、typets 为执行时间,d_time 为设备上报时间,create_time 为服务接收时间
app_kill_recordtsblack_list_id按策略标签、日期及设备条件筛选;SUM(frequency) 与 COUNT(*) 含义不同
server_monitortsserver_name单台服务器、明确时间窗口,累计指标和瞬时指标分开解释

业务查询窗口按表中限制设置。以 2026-09-18 为例,APP运行的8—9月走热库,7月及以前走归档库;冷热范围按自然月划分。

建议对首时间列使用明确起止范围,再加实际存在的标签条件;周期汇总优先查对应日/月/年结构。LIMIT 只约束返回结果,不替代扫描和聚合前的范围限制。标签过滤与时间过滤的基本用途见 TDengine 数据查询。

Elasticsearch:选对月份和字段类型​

设备大批量筛选使用 base_device;安装列表使用 app_install_deviceYYYYMM。它们承担检索,MySQL保留档案或快照,不能默认两端任何时刻都完全同步。APP运行明细使用TDengine,安装与运行是两类数据。

精确MAC、CPU或包名过滤,应使用该索引实际存在的 keyword 或 keyword 子字段;text 的分词匹配不是完整值相等。keyword 子字段存在 ignore_above 长度限制,不能默认所有长字符串都能精确检索。ES keyword 字段说明

各月安装 mapping 不完全相同:部分月份时间与CPU字段由 date/keyword 变为 text+keyword,跨月查询前先核对对应字段。apps 是JSON字符串,不存在 apps.packageName nested 查询路径;整串匹配不能代替“安装了某包名”的结构化筛选。

ee_default_alias 混有非安装对象且没有覆盖所有月份,不能代表完整安装历史。23个业务索引均为1个主分片。深分页或大批量导出应另设计稳定排序和分批范围,不能仅把页大小调大。

返回数据库清单

用户文档
AI 助手
Agent 列表
请选择一个 Agent 开始对话
AI 问答