如何建立双色球数据库模型

如何建立双色球数据库模型

建立双色球数据库模型,最稳妥的做法是把“期次、开奖号码、投注单、投注号码、中奖结果”拆成独立表,而不是把红球和蓝球全部塞进一个字符串字段。这样既能保存每期开奖数据,也能高效查询红球频率、蓝球走势、用户投注、中奖匹配和奖金统计。关键词“如何建立双色球数据库模型”的核心答案是:期次表管开奖事实,号码明细表管每个球,投注表管订单,投注号码表管选号,结果表管派奖。

一、先确定模型边界

可执行做法:先写清楚数据库要解决什么问题。如果只是做历史开奖查询,可以只建开奖相关表;如果要做投注系统、模拟选号、用户分析,就必须增加投注、用户和中奖结果表。双色球规则上,一注由 6 个红球和 1 个蓝球组成,红球范围是 1 到 33,蓝球范围是 1 到 16,因此数据库必须用约束防止越界号码进入。

如何建立双色球数据库模型

判断标准:如果你需要统计“01 号红球出现了多少次”,模型中就应该能直接按 red_number 分组;如果你需要判断某张票是否中奖,模型中就应该能把投注号码和开奖号逐个匹配。只靠 DrawNumbers 或 BetNumbers 这种逗号字符串,后期查询会变慢,也难以保证数据正确。

场景差异:个人分析项目可以简化为 drawings、draw_red_numbers、draw_blue_numbers 三张表;生产级投注系统应增加 bets、bet_red_numbers、bet_blue_numbers、bet_results、users、prize_tiers。注意事项是不要把“开奖数据”和“投注数据”混在一张表里,两者生命周期不同,开奖数据来自官方,投注数据来自用户行为。

二、推荐表结构

可执行做法:使用一个主表保存期次,再用子表保存每个红球和蓝球。红球和蓝球可以直接用号码作为业务值,不一定需要 RedBallId、BlueBallId 这种代理 ID。号码本身的范围很小,red_number 和 blue_number 更直观,也更适合统计。

  • drawings:保存期号、开奖日期、销售额、奖池金额等开奖级字段。
  • draw_red_numbers:一期开奖结果对应 6 行红球记录。
  • draw_blue_numbers:一期开奖结果对应 1 行蓝球记录。
  • bets:保存用户投注单、期次、投注类型、金额、状态。
  • bet_red_numbers:保存投注单选择的红球,可支持单式、复式、胆拖。
  • bet_blue_numbers:保存投注单选择的蓝球,可支持多蓝复式。
  • bet_results:保存中奖等级、命中红球数、是否命中蓝球、奖金。

判断标准:一个开奖期次应能查出刚好 6 个红球和 1 个蓝球;一个单式投注应能查出刚好 6 个红球和 1 个蓝球;一个复式投注允许红球大于 6 个或蓝球大于 1 个。场景差异在于,分析库更关心号码明细,交易库更关心投注单状态、支付流水和派奖状态。注意事项是 BetNumbers 字符串可以作为展示缓存,但不应作为唯一事实来源。

三、MySQL 建表示例

可执行做法:下面是一套偏分析和业务都能用的 MySQL 表结构。金额字段使用 DECIMAL,不使用 FLOAT;号码字段用 TINYINT;期号使用唯一索引,避免重复导入。

CREATE TABLE drawings (
  draw_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  draw_no VARCHAR(20) NOT NULL UNIQUE,
  draw_date DATE NOT NULL,
  sales_amount DECIMAL(14,2) NULL,
  pool_amount DECIMAL(14,2) NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_drawings_date (draw_date)
);

CREATE TABLE draw_red_numbers (
  draw_id BIGINT UNSIGNED NOT NULL,
  red_number TINYINT UNSIGNED NOT NULL,
  position_no TINYINT UNSIGNED NOT NULL,
  PRIMARY KEY (draw_id, red_number),
  UNIQUE KEY uk_draw_red_position (draw_id, position_no),
  CHECK (red_number BETWEEN 1 AND 33),
  CHECK (position_no BETWEEN 1 AND 6),
  FOREIGN KEY (draw_id) REFERENCES drawings(draw_id) ON DELETE CASCADE
);

CREATE TABLE draw_blue_numbers (
  draw_id BIGINT UNSIGNED NOT NULL,
  blue_number TINYINT UNSIGNED NOT NULL,
  PRIMARY KEY (draw_id),
  CHECK (blue_number BETWEEN 1 AND 16),
  FOREIGN KEY (draw_id) REFERENCES drawings(draw_id) ON DELETE CASCADE
);

CREATE TABLE bets (
  bet_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  draw_id BIGINT UNSIGNED NOT NULL,
  user_id BIGINT UNSIGNED NULL,
  bet_type VARCHAR(20) NOT NULL,
  stake_amount DECIMAL(10,2) NOT NULL,
  bet_status VARCHAR(20) NOT NULL DEFAULT '待开奖',
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (draw_id) REFERENCES drawings(draw_id),
  INDEX idx_bets_draw_status (draw_id, bet_status),
  INDEX idx_bets_user_time (user_id, created_at)
);

CREATE TABLE bet_red_numbers (
  bet_id BIGINT UNSIGNED NOT NULL,
  red_number TINYINT UNSIGNED NOT NULL,
  role_type VARCHAR(10) NOT NULL DEFAULT 'normal',
  PRIMARY KEY (bet_id, red_number),
  CHECK (red_number BETWEEN 1 AND 33),
  FOREIGN KEY (bet_id) REFERENCES bets(bet_id) ON DELETE CASCADE
);

CREATE TABLE bet_blue_numbers (
  bet_id BIGINT UNSIGNED NOT NULL,
  blue_number TINYINT UNSIGNED NOT NULL,
  PRIMARY KEY (bet_id, blue_number),
  CHECK (blue_number BETWEEN 1 AND 16),
  FOREIGN KEY (bet_id) REFERENCES bets(bet_id) ON DELETE CASCADE
);

CREATE TABLE bet_results (
  bet_id BIGINT UNSIGNED PRIMARY KEY,
  matched_red_count TINYINT UNSIGNED NOT NULL,
  matched_blue_count TINYINT UNSIGNED NOT NULL,
  prize_level VARCHAR(20) NULL,
  prize_amount DECIMAL(14,2) NOT NULL DEFAULT 0,
  settled_at TIMESTAMP NULL,
  FOREIGN KEY (bet_id) REFERENCES bets(bet_id) ON DELETE CASCADE
);

判断标准:导入同一期号时应被 UNIQUE 拦截;红球 34、蓝球 17 应被 CHECK 拦截;删除测试期次时,开奖号码明细应随主表删除。场景差异是 PostgreSQL 可把 AUTO_INCREMENT 换成 GENERATED ALWAYS AS IDENTITY,SQLite 可用 INTEGER PRIMARY KEY。注意事项是 MySQL 版本过旧时 CHECK 约束可能不生效,需要在应用层和导入脚本中再校验一次。

四、关系设计要点

可执行做法:所有明细表都通过外键指向主表。draw_red_numbers.draw_id 指向 drawings.draw_id,draw_blue_numbers.draw_id 指向 drawings.draw_id,bets.draw_id 指向 drawings.draw_id,bet_red_numbers.bet_id 和 bet_blue_numbers.bet_id 指向 bets.bet_id。这样可以保证没有期次的号码不会孤立存在,没有投注单的选号也不会孤立存在。

判断标准:关系设计是否合格,可以看三个查询是否简单:按期号查开奖号码、按号码统计历史频率、按投注单计算命中情况。如果这三个查询都需要拆字符串、正则匹配或大量应用层循环,模型就不够规范。

场景差异:如果只是爬取公开历史开奖,外键可以保留但不需要用户表;如果要接入真实用户账户,users 表和支付流水表应独立存在,不要把手机号、支付状态、奖金金额都塞进 bets。注意事项是不要建立“RedBallId 指向 BetNumbers”这种关系,字符串字段不能可靠表达多个外键关系。

五、数据类型选择

可执行做法:期次 ID 使用 BIGINT 或自增主键,期号 draw_no 用 VARCHAR(20),开奖日期用 DATE,金额用 DECIMAL(14,2),红球和蓝球用 TINYINT 或 SMALLINT。投注状态可以用 VARCHAR 加 CHECK,也可以建状态字典表。

判断标准:号码字段必须能表达范围并能排序;金额字段必须避免浮点误差;日期字段必须支持按月、按年、按区间查询。DrawNumbers 这类展示字段如果需要保留,建议命名为 display_numbers,并明确它只是冗余展示,不参与核心统计。

场景差异:MySQL 可用 TINYINT UNSIGNED,PostgreSQL 没有 unsigned 类型,可用 SMALLINT 加 CHECK。若使用 JSON 保存号码,PostgreSQL 可以配合 GIN 索引,MySQL 可考虑 JSON 多值索引,但这更适合补充字段,不适合作为最清晰的主模型。注意事项是彩票金额、奖池金额、投注金额都不应使用 FLOAT。

六、索引策略

可执行做法:优先给高频查询字段建索引。drawings.draw_no 必须唯一索引;drawings.draw_date 适合日期区间查询;draw_red_numbers(red_number, draw_id) 适合统计红球频率;draw_blue_numbers(blue_number, draw_id) 适合统计蓝球频率;bets(draw_id, bet_status) 适合查某期开奖后的投注状态;bets(user_id, created_at) 适合查用户投注历史。

CREATE INDEX idx_draw_red_number ON draw_red_numbers (red_number, draw_id);
CREATE INDEX idx_draw_blue_number ON draw_blue_numbers (blue_number, draw_id);
CREATE INDEX idx_bet_red_number ON bet_red_numbers (red_number, bet_id);
CREATE INDEX idx_bet_blue_number ON bet_blue_numbers (blue_number, bet_id);

判断标准:索引是否有效,要看 EXPLAIN 结果是否使用索引,以及查询耗时是否随数据增长保持稳定。场景差异是小型个人库几千期数据即使少量索引也能跑动,但生产库包含大量投注单时,投注明细表的索引会直接影响开奖结算速度。注意事项是索引不是越多越好,投注写入频繁时,过多索引会拖慢插入和更新。

七、导入与校验流程

可执行做法:每次导入开奖数据时,按“校验、开启事务、写入期次、写入红球、写入蓝球、提交事务”的顺序执行。红球必须正好 6 个、不能重复、范围在 1 到 33;蓝球必须正好 1 个、范围在 1 到 16;期号不能重复;开奖日期不能为空。

判断标准:导入脚本遇到重复期号时应跳过或更新,而不是插入第二条;遇到红球重复或越界时应记录错误并拒绝入库。场景差异是人工录入后台要给出明确错误提示,爬虫导入则要记录原始数据快照,方便排查来源页面变化。注意事项是不要硬编码“每周二、四、日一定开奖”作为唯一判断,实际入库应以官方发布的期号和日期为准。

八、常用查询示例

查询红球历史出现频率:

SELECT red_number, COUNT(*) AS frequency
FROM draw_red_numbers
GROUP BY red_number
ORDER BY frequency DESC, red_number ASC;

查询蓝球历史出现频率:

SELECT blue_number, COUNT(*) AS frequency
FROM draw_blue_numbers
GROUP BY blue_number
ORDER BY frequency DESC, blue_number ASC;

查询某一期完整开奖号码:

SELECT d.draw_no,
       d.draw_date,
       GROUP_CONCAT(r.red_number ORDER BY r.position_no) AS red_numbers,
       b.blue_number
FROM drawings d
JOIN draw_red_numbers r ON r.draw_id = d.draw_id
JOIN draw_blue_numbers b ON b.draw_id = d.draw_id
WHERE d.draw_no = '2024001'
GROUP BY d.draw_id, d.draw_no, d.draw_date, b.blue_number;

查询某期中奖投注:

SELECT b.bet_id, b.user_id, br.prize_level, br.prize_amount
FROM bets b
JOIN drawings d ON d.draw_id = b.draw_id
JOIN bet_results br ON br.bet_id = b.bet_id
WHERE d.draw_no = '2024001'
  AND br.prize_amount > 0
ORDER BY br.prize_amount DESC;

判断标准:频率查询不应依赖 LIKE ‘%01%’,因为字符串匹配会把 01、10、11 等场景搞混。场景差异是 MySQL 使用 GROUP_CONCAT,PostgreSQL 可用 string_agg。注意事项是号码展示时建议补零,例如 1 显示为 01,但数据库内仍保存整数,便于排序和校验。

九、中奖计算设计

可执行做法:开奖后用投注红球与开奖红球求交集,得到 matched_red_count;投注蓝球与开奖蓝球相同则 matched_blue_count 为 1,否则为 0。再根据奖级规则写入 bet_results。生产系统中,奖级规则建议放进 prize_tiers 表,包含 prize_level、required_red_count、required_blue_count、fixed_amount、is_floating 等字段。

判断标准:同一张投注单只能结算一次,bet_results.bet_id 使用主键可以防止重复派奖;结算后 bets.bet_status 应从“待开奖”变为“已结算”或“未中奖”。场景差异是模拟分析可以实时计算不落库,真实投注系统必须落库并保留 settled_at。注意事项是复式和胆拖投注可能对应多组组合,结算前要明确 bets 表表示“原始票面”还是“拆分后单注”,否则奖金计算会混乱。

十、什么时候可以用字符串或 JSON

可执行做法:如果只是做静态展示,可以在 drawings 中增加 display_numbers,例如“01,05,12,18,22,31 + 09”。如果需要保存原始接口返回,可以增加 raw_payload JSON 字段。但统计、匹配、派奖仍应以明细表为准。

判断标准:如果一个字段只是展示给用户看,用字符串可以接受;如果一个字段要参与 WHERE、JOIN、GROUP BY、COUNT,就应拆成行。场景差异是低成本博客演示可以用字符串方案快速上手,长期分析系统应使用规范化模型。注意事项是 JSON 虽然灵活,但约束和跨数据库兼容性较弱,不能替代清晰的关系模型

十一、扩展到其他彩票游戏

可执行做法:把“游戏”抽象出来,增加 lottery_games 表,字段包括 game_id、game_name、red_min、red_max、red_count、blue_min、blue_max、blue_count。drawings 和 bets 增加 game_id 后,就能支持不同彩票玩法。

判断标准:新增一种玩法时,如果只需要增加配置和奖级规则,不需要重写表结构,说明模型扩展性较好。场景差异是只做双色球不必过早抽象;如果一开始就要支持大乐透、3D、七乐彩,则应从第一版就加入 game_id。注意事项是不同彩票的号码区、奖级、投注方式差异很大,不要假设所有游戏都只有红球和蓝球。

常见问题

1. 双色球号码能不能直接存成“01,02,03,04,05,06|07”?
可以作为展示字段,但不建议作为核心存储。只要你要做频率统计、命中计算或索引优化,就应该拆到 draw_red_numbers 和 draw_blue_numbers。注意展示补零和数据库整数存储要分开处理。
2. 红球表和蓝球表一定要单独建 RedBalls、BlueBalls 吗?
不一定。红球范围只有 1 到 33,蓝球范围只有 1 到 16,直接用 red_number、blue_number 加 CHECK 约束通常更简单。如果系统需要维护号码元数据,比如颜色、排序、玩法配置,再建号码字典表。
3. 开奖号码里出现重复红球怎么办?
正常规则下,同一期红球不应重复。数据库应使用 PRIMARY KEY(draw_id, red_number) 或唯一约束拦截重复号码。若导入时发现重复,优先判定为采集、解析或人工录入错误,不应静默入库。
4. 想分析冷热号,最重要的索引是什么?
最重要的是 draw_red_numbers(red_number, draw_id) 和 draw_blue_numbers(blue_number, draw_id)。它们能支持按号码分组、按时间过滤后的频率统计。数据量不大时差异不明显,但历史数据和投注数据增长后会很关键。
5. 用户信息应该放在投注详情表里吗?
不建议。投注表只保存 user_id,用户姓名、手机号、账户状态应放在 users 表。这样能减少重复数据,也便于权限控制和隐私处理。生产环境还应把支付流水、派奖流水与 bets 分开。

结论

建立双色球数据库模型的关键不是多建表,而是把可分析的数据拆到正确粒度:一期一条开奖主记录,一个球一条号码明细,一张投注单一条投注主记录,一个选号一条投注号码明细。判断模型是否合格,只看三件事:能否防止错误号码入库,能否快速统计号码频率,能否可靠计算投注中奖结果。满足这三点,后续无论做历史走势、用户投注分析还是多种彩票扩展,都不会被早期表结构卡住。

参考文献

原创文章,作者:王利头,如若转载,请注明出处:https://www.wanglitou.cn/article_23106.html

(0)
打赏 微信扫一扫 微信扫一扫
上一篇 2024-04-25 18:02
下一篇 2024-04-26 08:22

相关推荐

公众号