我不知道你用的是什么数据库,我会用 Postgres,因为它是免费的,遵循 SQL 标准,有很好的文档,而且非常强大。
正如 Gordon 所说,您似乎拥有三样东西:用户、膳食和位置。三样东西意味着每样东西一张桌子。这避免了存储冗余数据。整个话题是database normalization。
create table users (
id bigserial primary key,
username text not null unique,
email text not null unique,
first_name text not null,
last_name text not null
);
create table meals (
id bigserial primary key,
type text not null unique,
-- If calories vary by user, move this into user_meals.
calories integer not null
);
create table locations (
id bigserial primary key,
-- The specific information you have about a location will vary,
-- but this is a good start. I've allowed nulls because often
-- people don't have full information about their location.
name text,
address text,
city text,
province text,
country text,
postal_code text
);
您询问了复合键。不要打扰。潜在的问题太多了。在每个表上使用一个简单、唯一、自动递增的大整数。
主键必须是唯一且不变的。用户名、姓名、电子邮件地址……这些都可以改变。即使您认为他们不会,为什么要将风险纳入您的架构?
外键将在整个数据库和索引中重复多次。您希望它们小而简单且大小固定,以免占用不必要的存储空间。整数小而简单且大小固定。文字不是。
复合主键可能会泄露信息。主键指的是一行,这些通常出现在 URL 等中。如果我们使用用户的电子邮件地址或姓名可能会泄露personally identifiable information。
我选择了bigserial 作为主键。 serial 类型是自动递增的,因此数据库将负责为每个新行分配一个主键。 bigserial 使用 64 位整数。一个普通整数只能容纳大约 2 到 40 亿个条目。这听起来很多,但事实并非如此,而且您不想用完主键。 bigserial 可以处理 9 quintillion。每行额外的 32 位是值得的。
还有一些笔记。
- 除非我们有充分的理由,否则一切都是
not null。这将捕获数据输入错误。它使数据库更易于使用,因为您知道数据会在那里。
- 同样,任何应该是唯一的都被声明为
unique,所以它是有保证的。
- 对列大小没有任意限制。您可能会看到其他示例,例如
email varchar(64) 等。这实际上并没有为您节省任何空间,它只是限制了数据库中允许的内容。用户名或电子邮件地址的长度没有基本限制,即business rule。业务规则无时无刻不在变化;它们不应该被硬编码到模式中。执行业务规则是输入数据或触发器的工作。
现在我们有了这三张表,我们可以用它们来记录人们的用餐情况。为此,我们需要第四张表来记录用户在某个位置用餐:一个连接表。连接表允许我们将有关用户、膳食和位置的信息保存在一个规范的位置。用户、位置和餐点由称为外键的 ID 引用。
create table user_meals (
id bigserial primary key,
user_id bigint not null references users(id),
meal_id bigint not null references meals(id),
location_id bigint not null references locations(id),
-- Using a time range is more flexible than start + duration.
taken_at tstzrange not null,
-- This can store up to 99999.99 which seems reasonable.
cost numeric(7, 2) not null,
notes text,
-- When was this entry created?
created_at timestamp not null default current_timestamp,
-- When was it last updated?
updated_at timestamp not null default current_timestamp
);
- 因为我们使用
bigserial 作为所有内容的主键,所以引用其他表很简单:它们都是bigint。
- 和以前一样,除非我们有充分的理由,否则一切都是
not null。例如,并非每餐都会有notes。
- 相当标准的
created_at 和updated_at 字段用于记录条目的创建或更新时间。
- 避免使用不精确的数字类型,例如
float 或 double,尤其是对于金钱而言。相反,arbitrary precision type numeric 用于精确存储资金。
-
numeric 要求我们给它一个精度和规模;我们必须选择一个限制,所以我选择了一些不合理的高我货币的东西。您的货币可能会有所不同。
- 我们利用Postgres's range types 存储从用餐开始到用餐结束的时间范围,而不是存储开始时间和用餐时间。这使我们可以使用range functions and operators 来更简单地查询用餐时间。例如,
where taken_at @> '2020-02-15 20:00' 查找在 2 月 15 日晚上 8 点吃的所有餐点。
还有更多工作要做,例如adding indexes for performance,希望这可以帮助您入门。如果有一个外卖,那就是:不要试图把所有东西都塞进一张桌子。
Try it out.