oriana这个项目名字,我一记就是好几年。它不是开源框架,也不是哪家云厂商的产品,而是我从2014到2023年一步步搭起来的大陆、港股上市企业数据库,前前后后折腾了近十年,数据量从最初几万行滚到了几千万行。当年做区域资本市场研究,最头疼的不是模型怎么搭,而是数据散得太可怕:A股一套报表、港股一套报表,披露口径、币种、财年全都不一样;十年期历史数据更是要翻各种渠道才拼得齐。这个项目解决的就是这问题,把两套市场的核心企业数据,统一成一个可查询、可校验、可回溯的数据库。如果你也在做金融研究、量化回测、课题分析、或者公司基本面数据仓库建设,这份从零开始的经验应该能帮你少走不少弯路。
1. 项目定位与整体设计思路
1.1 为什么要花十年攒一个跨市场上市企业数据库
我先说清楚这个项目最开始是解决什么问题的。当时团队手上要研究2014年之后的大中华区企业变化,需要覆盖大陆A股和港股两个市场。传统做法是去wind、choice这些终端里导数据,但终端导出的数据有几个麻烦:一是字段格式经常变,今天导出来是这个结构,明天就换了;二是跨期数据有断档,尤其是港股老公司的历史财务数据,终端里也不全;三是授权和账号限制,多个团队成员同时用容易冲突。与其每次都被外部工具牵着走,不如自己沉淀一套内部可控的数据库。
这个项目的核心目标,是把2014到2023年两个市场的上市企业信息完整地收进自己的库:公司基本信息、财务三张表、股本和高管、日线行情、行业分类,全部统一口径。一次建好,后续做策略回测、写研究报告、跑数据模型,都从自己库里取数。它的定位不是做一个展示型网站,也不是做一个简单的Excel台账,而是一个能支撑多人并发查询、能方便做时间序列分析、能跨市场做对比的专用数据库。对刚接触的同学来说,这套设计思路,说白了就是在“把数据当成资产来管理”而不是“把数据当成文件存起来”。
1.2 整体架构选型:为什么我坚持分层而不是一把梭
数据库项目最容易翻车的地方,是刚开始就直接建表,然后边导入边改结构,最后表结构乱得没法看。我在oriana项目里一开始也犯过这个毛病,后来才稳定成三层架构:源数据层、基础数据层、应用层。
源数据层存的是从公开渠道抓回来的原始文件,包括交易所公告、财报PDF、行情快照,这些文件基本不做加工,只按日期和来源归档,方便出问题时回溯。基础数据层才是数据库的核心,存的是清洗、标准化之后的表结构,比如统一了货币单位、统一了财报口径的公司财务表。应用层则是为了查询效率准备的视图、汇总表和宽表,比如“某年某行业平均市盈率”这种指标,都是预先算好,不跑原始大表。这样分层最大的好处是,上层数据出了问题,不用动最底层的原始数据,重跑一遍清洗脚本就行。如果你打算做类似的数据项目,我建议也按这个思路来,别想着一步到位建一张万能大宽表,那是数据库设计里的坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据建模:表结构和字段设计的核心逻辑
2.1 实体关系怎么拆:从公司主表出发
数据建模是oriana项目里最花心思的部分。整个库我拆成了六类核心实体:公司主表、财务报表、股本结构、高管薪酬、日线行情、行业分类。这六个实体之间不是平铺的关系,而是围绕“公司”这个主主体展开。公司主表存的是股票代码、上市状态、注册地、实际控制人这些不变的静态信息;财务报表表按报告期存储,一行就是一家公司一个报告期的一张报表;股本结构表跟踪每家公司历年股本变化;行情表存交易日的数据。
这里有个基础但重要的设计决策:公司主表必须分开,不要和财务数据混在一张表里。这就像一个员工的个人信息和工资流水要分开,不然每加一条流水就重复一遍个人信息,占空间不说,还特别容易产生数据不一致。用一句话概括,表设计的原则是:把变化频率低的信息单独拆表,把高频变化的数据按事件维度存储。另外,每个表我都加了主键和唯一键,比如财务报表表用“股票代码+报告期+报表类型”作为唯一约束,从根源上防止重复导入。
2.2 跨市场字段对齐:大陆和港股的公司数据怎么放到同一套结构里
这是整个项目最磨人的部分。A股和港股虽然都是上市公司,但披露规则差别很大,直接塞进同一张表必然出问题。
先说股票代码。A股是6位数字,港股是5位数字,而且两个市场都有过代码重用的历史情况。我在公司主表里单独存了交易所字段,用字符串“SSE”“SZSE”“HKEX”区分,股票代码也统一存成带后缀的完整代码,比如“600519.SH”和“00700.HK”,这样后续关联行情和财务数据时不会串场。
再说财报周期。A股公司财年基本统一为1月1日到12月31日,港股虽然也多数按自然年,但少数公司的财年截止日是3月31日、6月30日甚至12月31日前后有区别。所以在财务表里我只存“报告期”这个字段,而不是简单存“年份”。比如一家港股公司2023财年报告期是2023年3月31日,那它就是2023财年数据,不能因为发布日期在2023年6月就归入2023自然年。
还有股本和薪酬。港股有普通股和不同投票权股份之分,同样一家公司,股本结构和A股完全不是一回事;高管薪酬方面,港股要求披露个别董事的薪酬区间和明细,A股通常披露公司高管的薪酬总额。这些差异没法强行统一到一个字段。我的处理方案是:在每个口径有差异的字段前面加“披露口径”标识,比如“高管薪酬总额”和“高管薪酬明细”分两个字段,宁可多存几列,也不要为了省空间把不同口径的数据混在一个值里。
2.3 单位、汇率和会计口径的统一:看似简单,实际上最容易出错
做金融数据库的人都知道,口径不统一比数据缺失更可怕。这个项目里,我统一做了三件事。
第一是金额单位统一。A股财报单位有“元”“万元”“亿元”三种,港股通常是千港元、百万港元。我在清洗脚本里做了一级转换,入库之前全部统一成“元”这个基础单位,避免查询时还要带上单位判断。财务报表里我特地把“报表货币”字段保留下来,例如HKD、CNY,方便以后做汇率换算核对。
第二是汇率处理。港股公司财报货币是港币,但做跨市场对比时,得全部折算成人民币。汇率取数我固定用“报告期期末汇率”而不是交易日的波动汇率,这样同一个报告期的财务数据无论什么时候查,结果都是稳定的。如果你直接存原始港币数值,等汇率变了再折算,同一份财报在不同时间算出来的对比结果会飘,这是研究里的大忌。
第三是会计口径。A股用的是中国企业会计准则,港股基本上是香港财务报告准则,和国际财务报告准则基本趋同。两个准则下,资产减值、收入确认的细节会有差异。我没有强行把会计准则改成一套,而是在每张财务表里保留一个“准则来源”字段,查询时可以通过这个字段做过滤。如果业务场景是纯量化分析,可以考虑只保留统一口径后的指标,但要清楚这背后已经做过一次不可逆的加工。
3. 数据采集与清洗的实操流程
3.1 数据来源规划:公开渠道怎么组合
oriana项目的原始数据,主要来自这几个公开渠道:两个交易所的官方网站、上市公司披露的定期报告PDF、主流财经网站提供的历史数据接口,以及部分愿意开放数据的第三方金融数据接口。我没有用爬虫专门去抓互联网上零散的数据,因为来源太杂的金融数据你没法判断它到底被转了几手。
我的建议是,数据源遵循“官方优先、接口辅助、多源交叉”的原则。年报PDF和交易所公告是权威来源,但解析成本高;财经接口和第三方API方便,但可能存在延迟或截断;这两种来源互补,比如财务表字段以官方PDF解析为主,行情数据直接用日线接口拉取。每个表里我都留了“来源”和“更新时间”两个字段,这样出现数据疑问时,能追溯到是哪个渠道来的数据。
3.2 清洗与去重:脏数据是如何一步步处理掉的
数据清洗是整个项目里最耗费时间的环节,估计占了六成工时。从原始数据到能入库的干净数据,要经过四步。
第一步是格式统一。把来源里所有日期字段变成统一格式,比如“2023-12-31”,把所有金额单位转换为“元”,把所有百分数转成小数,把“--”和空值统一为NULL。第二步是去重,这个非常关键。同一家公司的同一份财报,可能在多个来源都出现过,如果不去重,统计报表时会出现双重计数。我的去重逻辑很简单,在插入时利用“股票代码+报告期+报表类型”的唯一索引直接拒绝重复行,同时保留更新能力,以官方来源的数据为准做幂等更新。第三步是缺失值处理,对每个字段统计缺失率,缺失率超过50%的字段单独分析,而不是直接填充。第四步是异常值识别,比如营收为负、总资产为0、员工数缺失但财报有披露,这些异常值先用“标志字段”标记出来,不直接删除,让研究者自己决定要不要剔除。
清洗过程里特别提醒一件事:不要轻易修改原始数据。我的清洗脚本只会输出新表,原始抓取的表基本只做追加,不UPDATE。万一后面发现某个清洗逻辑有问题,还能从原始数据重跑,而不用回滚数据库。
3.3 数据校验:用勾稽关系卡住错误数据
即使做了清洗,也不能保证数据就是对的,所以我加了数据校验环节。这里用到的思路主要是会计上的勾稽关系和业务上的常识约束。
财务表的校验包括:资产负债表里的资产端必须等于负债加所有者权益,差一分钱都说明数据解析有问题;利润表里的营业收入、营业成本、净利润要满足基本逻辑,比如净利润不能超过营业收入的数量级太多;现金流量表的期末现金余额要与资产负债表里的货币资金匹配。行情表的校验包括:当天的最高价必须大于等于最低价和收盘价,收盘价和涨跌幅要能互推。行业分类的校验包括:同一家公司同一时间不能有两个行业分类,除非是多元化业务的特殊情况,都要标记。
这些校验规则不用每一条都写成特别复杂的存储过程,我用的是Python脚本定期跑,把异常记录输出到一张“数据质量日志表”。然后每天定个固定时间检查一下这条日志,把报错数据重新标记、重新清洗。如果你是把这套流程用在生产环境,我建议把校验规则写成自动化测试,每星期跑一次,别等到最后写报告的时候才发现某年数据是错的。
4. 数据库选型与建库实现
4.1 数据库选型:MySQL、PostgreSQL与国产数据库怎么权衡
oriana项目最初用的就是MySQL,后来因为项目里又跑了一些时序类的行情分析,我也在部分场景尝试过PostgreSQL,再后来因为信创要求,还专门测试过达梦、人大金仓、GaussDB这类国产数据库的兼容性。我自己的体会是:如果你的核心需求是大量的关联查询、事务处理、数据一致性强,MySQL或PostgreSQL完全撑得住;如果项目有强制要求,达梦、人大金仓这类国产数据库在基础SQL语法上兼容MySQL或Oracle做得已经相当不错,但有一些细节需要注意。
对比一下我实际使用过的几个方案:
| 数据库 | 优势 | 注意点 | 适合场景 |
|---|---|---|---|
| MySQL 8.x | 生态成熟、运维资料多、连接池方案多 | 复杂分析查询性能一般 | 传统OLTP、中小规模金融数据 |
| PostgreSQL | JSON支持好、窗口函数强、扩展丰富 | 默认配置需要调优 | 需要复杂统计和文本检索的场景 |
| 达梦DM8 | 兼容Oracle语法、支持冷迁移 | 第三方工具链支持略弱 | 有国产化要求的项目 |
| 人大金仓 | 兼容性好、部署简单 | 如果不启用SSL,部分客户端会报错 | 有国产化要求的项目 |
| GaussDB | 华为系生态、大数据场景性能好 | 社区资料相对少、不建议新手直接用 | 大数据量联机分析 |
我当时主库最终用的是MySQL,把原始文件放在文件系统,分析副本放在PostgreSQL。这不是说MySQL比别的强,而是我团队对MySQL的运维经验最多,遇到问题好排查。数据库选型不要“追新”,要选团队维护成本最低的那个。
4.2 建表SQL与索引设计:别让查询慢出天际
这里我给一个金融数据库里最常用的财务报表表的建表示例,结构大概是这样的:
sql复制CREATE TABLE financial_report (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
stock_code VARCHAR(20) NOT NULL,
report_period DATE NOT NULL,
report_type VARCHAR(20) NOT NULL COMMENT 'balance/profit/cashflow',
currency VARCHAR(10) DEFAULT 'CNY',
revenue DECIMAL(20,2),
net_profit DECIMAL(20,2),
total_assets DECIMAL(20,2),
total_liabilities DECIMAL(20,2),
equity DECIMAL(20,2),
source VARCHAR(100),
data_quality_flag TINYINT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_stock_period_type (stock_code, report_period, report_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
建表时有两个细节很关键。一是decimal类型不要用float,金额精度会被截断,几万亿的资产数据,float误差会放大到不可接受。二是唯一索引一定要建,它能从数据库层面阻止重复数据。但索引也不能贪多,尤其是联合索引,我建的时候会问自己:这条索引到底为哪个查询服务?有没有被其他索引覆盖?如果查不出来,就直接不建。
索引设计上有几个经验。股票代码+报告期的联合索引是最常用的,因为几乎所有查询都会按这个组合过滤。日期字段单独建索引,因为行情表和财报表经常按报告期做范围查询。但文本类字段,比如公司全称,不要直接加普通索引,要么用前缀索引,要么配合全文索引,否则索引占用空间大还起不到加速效果。最后一点,索引字段的顺序很重要,最左前缀原则下,最常过滤的字段放最前面,比如在联合索引里,把“stock_code”放前面通常比把“report_period”放前面更适合多数查询。
4.3 并发写入与连接池:多线程入库时为什么会出现死锁
oriana项目里有一个阶段是用多线程并发抓取和写入数据,结果经常出现死锁。最典型的现象是写日志里出现“Deadlock found when trying to get lock; try restarting transaction”。我开始还以为是数据有问题,后来才定位到,是多个事务在同一个表里以不同顺序加锁。
这个问题的本质是:两个线程同时往里插数据,一个先锁定A记录,再锁定B记录;另一个反过来先锁定B再锁定A,两个线程互相等对方释放锁,数据库就只能报死锁。解决办法有几个,最有效的是让所有线程按同一个顺序处理数据,比如都按股票代码排序后再开启事务写入。还有一个稳妥办法是减小事务范围,一次事务只提交一个批次的数据,而不是一个大事务处理十万行。事务粒度越小,锁持有的时间越短,死锁概率越低。
连接池方面,我当时用Druid连接池,核心参数是这样配的:initialSize=10,minIdle=10,maxActive=100,maxWait=60000。有个容易被忽略的参数是maxWait,设得太短容易被高并发打爆连接数,设得太长又会积累一堆等待线程。60秒是我多次压测下来比较稳的值。连接池的另一个作用是复用连接,避免每次查询都新建连接。同时,连接池里要开“空闲连接检测”,不然MySQL服务端如果没有特殊配置,空连接会被数据库侧断开,程序还在用旧连接去查询,就会出现“Connection is not available”这类报错。
4.4 审计、SSL与安全配置:别小看这些基础项
安全配置往往是在项目上线一段时间后才被重视。oriana项目就遇到过一个典型的坑:数据库开启了审计功能后,系统的整体查询性能明显下降,后来排查发现是审计日志在频繁写入时和普通查询争用磁盘IO和索引资源。审计确实能追溯数据修改记录,但金融数据库不需要对每条查询都审计,只需要对UPDATE、DELETE这类敏感操作开启审计就够了。如果像我当时那样图省事全开,就是给性能挖坑。
另一个容易踩的坑是SSL。我用DBeaver连接人大金仓数据库时,出现过“连接失败,提示SSL未启用”的问题。查下来是客户端默认要求SSL加密连接,而数据库服务端没开启SSL或没配证书。解决办法有两种:要么在服务端生成自签名证书并配置SSL;要么在客户端连接字符串里明确设置sslMode=disable,但这个只适合内网测试环境,生产环境还是建议把SSL配上。另外,MySQL 8默认的认证插件是caching_sha2_password,很多老客户端连不上,如果你用的工具版本比较老,连接时提示认证失败,需要在MySQL里创建对应账号时指定mysql_native_password,或者升级客户端驱动。
5. 开发工具与日常运维
5.1 开发工具选型:DBeaver和dbx到底哪个好用
数据库建好之后,开发和日常运维的工具也很影响效率。oriana项目里我自己用得最多的是DBeaver,它是免费开源的,支持MySQL、PostgreSQL、达梦、人大金仓、GaussDB等几乎所有主流数据库,连接管理、ER图、SQL执行计划都有。DBeaver的数据库迁移功能也方便,可以在不同库之间搬表,但它有个需要警惕的点:大数据量表的迁移很容易超时,迁移前要调整连接的超时时间参数。
dbx工具是另一个我用来做命令行管理的轻量工具,适合在服务器上做快速的数据库操作和导出脚本任务。对刚接触数据库的人来说,DBeaver的图形界面更友好一些;但对经常做自动化管道的人来说,命令行工具更顺手。我的建议是两者配合使用,日常查询用DBeaver,批量脚本和定时任务用命令行工具。还有一个容易忽略的点:用DBeaver连接数据库时,如果连的是内网跳板机后的库,连接配置里要把“服务器时区”和“连接超时”设对,否则经常报连接超时或者时间“The server time zone value”的异常。
5.2 批量导入Excel/CSV:从“外部表格式错误”说起
项目里有一部分历史数据是从Excel和CSV导入的,这里面踩过的坑不少。最常见的报错是“连接到数据库失败。常规功能故障外部表不是预期的格式”,这个报错虽然经常出现在ArcGIS连接Excel场景里,但本质和普通数据库导入Excel报错一样,都是格式不对。Excel文件并不是严格的数据库格式,尤其是包含合并单元格、表头层级、科学计数法、中文字符集问题的时候,数据库很难直接解析。
我的操作流程是先做预处理,不直接拿原始Excel入库,而是先把Excel另存为CSV(UTF-8编码),然后在导入前用Python脚本对CSV做一轮字段长度检查。比如某列原始数据超过了MySQL字段定义长度,导入就会在中途报错。大批量导入时,直接使用LOAD DATA INFILE比逐条INSERT快几十倍,但要注意文件编码和数据分隔符。如果遇到某些行特殊字符导致解析错位,我会先用文本编辑器打开CSV检查一下,不要盲目调参。对于总量几十万行的文件,这个流程完全能跑;超过几百万行,建议拆成多个小文件分批导入,避免单个事务过大导致锁等待或者内存溢出。
5.3 慢SQL与索引失效:查询优化怎么一步步排查
数据库跑了一段时间后,我开始收到“某些查询特别慢”的反馈,尤其是一些带时间范围过滤的统计SQL。排查慢查询的第一步是打开慢查询日志,或者直接在DBeaver里执行EXPLAIN查看执行计划。我遇到过最典型的问题是“索引列上套了函数导致索引失效”,比如查询里写了WHERE YEAR(report_period)=2023,而没有写成WHERE report_period BETWEEN '2023-01-01' AND '2023-12-31',前者因为对字段做了函数运算,MySQL会放弃索引,只能全表扫描,慢是必然的。
另一个常见的坑是隐式类型转换。我在设计表时把stock_code定义成了varchar,但有些查询代码里写了WHERE stock_code=600519,数字600519会被转换成字符串,但如果字段里存的还有“600519.SH”,这个等值匹配就命中不了。还有一种是OR条件导致索引失效,比如WHERE stock_code='600519.SH' OR report_type='profit',优化器可能直接放弃索引,改成UNION ALL或者用IN来替代。索引优化这事没有捷径,就是多练,多结合EXPLAIN看,我最近的一个心得是,不要为了“覆盖所有业务场景”而建一堆索引,保留最常用的几条,反而会让后续的维护和写入性能更稳定。
6. 常见问题与排查技巧实录
6.1 数据库连接和文件格式问题速查
这类问题我在QQ群和社区回答过不少,整理成一张速查表,方便各位对照排查。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 客户端提示“无法连接到数据库服务器” | 网络不通、端口未开放、服务未启动 | 先ping主机,再telnet端口,最后确认服务状态 |
| 连接报“Access denied for user” | 用户名密码错误、权限不足 | 用管理员在服务端查看并授权 |
| 连接报“The server time zone value” | MySQL时区配置不对 | 连接串加serverTimezone=Asia/Shanghai |
| 导入Excel提示“外部表不是预期的格式” | 文件不是真正的xlsx/xls,或包含复杂格式 | 另存为CSV,预处理后再导入 |
| 提示“主数据库无法访问” | 高可用架构中主库故障或连接池失效 | 检查主从状态,重启数据库前先抓日志 |
| 人大金仓连接报SSL未启用 | 服务端未启用SSL或客户端强制SSL | 服务端生成证书配置SSL,或客户端设置sslMode=disable |
| oracle安装或迁移时报字符集不一致 | 源库和目标库字符集不同 | 迁移前统一字符集,必要时用冷迁移 |
这里我想多说一句,做数据库排查时,先看日志再看配置,不要上来就重启。很多问题重启之后会“暂时消失”,但根本原因还在,后面会以更奇怪的方式冒出来。
6.2 死锁与锁等待的高效排查
如果项目里并发写入一直没有规范好,死锁问题就会反复出现。死锁的排查,不要只盯着报错信息,而是要去看数据库的锁状态。MySQL里可以用SHOW ENGINE INNODB STATUS查看最近一次死锁的细节,里面会直接给出两个事务互相持有哪些锁、在等哪些锁。我曾经靠这个命令定位到的问题甚至不是代码或SQL本身的锅,而是两个定时任务在同一个时间段跑了两个大事务,互相更新了对方先锁定的行。
还有一类问题是锁等待超时,和死锁不一样,锁等待是一个事务一直持锁不释放,另一个事务在等待它,等待超过设定时间(默认50秒)就报错。排查时用performance_schema.data_lock_waits或者直接查information_schema.innodb_trx,看看哪个事务一直处于RUNNING状态且执行时间特别长。这类问题的常见诱因是:事务里做了外部接口调用、大批量数据更新没分批、事务忘了COMMIT。解决办法也简单:事务代码里不要放远程调用;大批量更新按主键ID分批,每批500到1000条;检查所有连接池获取的连接事务能不能及时释放。
6.3 数据一致性和重复导入如何预防
最后一块是我觉得对做数据类项目的人最有用的:数据一致性。oriana项目里的核心经验就是,预防比补救更重要。数据库层面的唯一约束就是最强的预防措施,financial_report表的唯一索引是uk_stock_period_type,它直接保证了同一家公司同一个报告期同一种报表类型只能有一行。这个约束加好之后,就算清洗脚本有bug、多线程重复跑了一遍,数据库本身也会拒绝重复插入。
但有些重复不是完全重复,而是“半重复”:比如两家数据源对同一家公司的同一报告期营收数据不一致,唯一索引在这里就拦不住了。这种情况我靠的是“数据来源优先级”策略,在插入时如果发现冲突,就按优先级排序,只保留官方PDF解析的数据,其他来源的数据写入日志表留档而不是覆盖。这样做的好处是,数据永远有可追溯性,就算后来发现官方PDF解析错了,也能对比其他来源的数据做修正。最后再用我之前提过的数据校验脚本做周期检查,比如统计每个报告期的记录数会不会异常翻倍、关键指标的最大最小值有没有超出合理范围,保持一个良性循环。
如果说这个项目让我感触最深的一件事,那就是做数据库,慢就是快。前期多花时间在表设计和数据校验上,后面每一天的查询开发、研究分析都会变得顺畅;前期图省事,后面每一个环节都在还债。如果你正准备搭自己的金融数据库,或者只是想把一堆Excel整理成可查询的结构,希望这份经验能给你省下几个月的弯路。
