需求背景

我们正在构建一个SDK,它通过在正确的时间向正确的人展示正确的产品,从而帮助应用程序开发人员增加收入。

为了提供这项服务,我们需要跟踪客户的所有用户,即最终用户。为简单起见,假设我们与五家公司合作,每家公司每天有约五千次下载,即每天大约有两万五千次下载,或者说一个月有七十五万次用户下载。为跟踪所有这些用户,我们将记录存储在一个类似于MySQL数据库的“users”表中。

我之所以说类似于MySQL,是因为我们实际上使用的是PlanetScale的无服务器数据库产品。之所以PlanetScale,是因为我们看到了大量优秀的GitHub工程师来到PlanetScale,以及看到GitHub发布简单迁移的过程后,我们选择了它。

我们的表结构

为了跟踪用户,我们创建了两个表,比如ApplicationUser和ApplicationUserAlias。ApplicationUserAlias与ApplicationUser之间存在多对一关系。我们同时需要这样两个表,在客户告诉我们用户是谁之前,我们为用户分配了一个随机id来跟踪事件。所以我可能被称为$SuperwallAlias:…和custom_id190390930。

CREATE TABLE `ApplicationUser` (
`id` int NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`),
) ENGINE=InnoDB;

CREATE TABLE `ApplicationUserAlias` (
`id` int NOT NULL AUTO_INCREMENT,
-- Some identifier by which we know the user, a lot of apps use uuids
`vendorId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
-- The "foreign key" back to ApplicationUser
`applicationUserId` int NOT NULL,
PRIMARY KEY (`id`),
) ENGINE=InnoDB;

没有外键

PlantScale存在一个怪癖,或者更具体地说是Vitess缺少对外键的支持。外键基本上与“在线模式迁移”不兼容,这是一种在没有任何锁定的情况下就可以更改数据库模式的奇特方式。没什么大不了的,有很多变通方法。

但是,没有外键的一个后果是没有自动创建索引。“MySQL要求对外键列进行索引;如果你建的表具有外键约束,但相应的列上没有索引,则会创建索引。”(参见https://dev.mysql.com/doc/refman/8.0/en/constraint-foreign-key.html。)

在不知情的情况下,我一直依赖MySQL在外键上自动创建的索引来加快查询速度。它通常用于查找一个父记录的所有子记录,在本例中就是一个用户的所有别名。

我们正在运行下面的查询来查找一个用户的所有别名。有了自动外键索引,该查询就“廉价”了,因为我们只需通过applicationUserId进行查找。没有外键索引,这是一个全表扫描:

SELECT id, vendorId FROM ApplicationUserAlias WHERE applicationUserId = x LIMIT 100;

“读取行数”

对于大多数数据库提供商来说,该错误只会导致查询速度变慢。无论如何,直到在收到账单之前,我们都没有注意到PlanetScale的这个问题。

PlanetScale的定价模型基于“读取行数”,我在脑海中一直将其视为“返回行数”。在上个示例中,返回最多100行ApplicationUserAlias,即每返回1000万行1.5美元,我们可以轻松负担返回的100行,或者我认为是这样。

仔细查看定价页面,read的定义是“在对PlanetScale数据库进行任何类型的查询或更新时所检索或返回的行”。(参见https://docs.planetscale.com/concepts/billing。)

最重要的词是“检索”,因此,我们查找100个别名的简单查询实际上是在检索整个users表。在我们使用服务的第一个月,该表就已经超过了100万行。

每次查询请求都要花费0.15美元,反观我们的数据,我们每小时向该端点发出约280个请求,这与PlanetScale中的账单相匹配,大约为每天一千美元。

修复

修复实际上非常简单,只需要手工创建MySQL为我们自动创建的外键索引。

CREATEINDEX `ApplicationUserAlias.applicationUserId_index`  ON`ApplicationUserAlias`(`applicationUserId`);

我们何时添加的索引?

要想弄明白这一点,我使用了EXPLAIN ANALYZE,但这是另一篇文章。

目前摆脱困境

PlanetScale的精英们非常乐意让我们摆脱这次的困境,并已将我们的账单记入贷方,使我们恢复到比以前更合理的价格,每月约150美元。

我真的对他们正在做的工作感到非常兴奋,我认为我们的选择仍然是有意义的,即使我们不得不适应一些变化。