数百万行不是问题,这就是 SQL 数据库旨在处理的问题,如果您拥有设计良好的架构和良好的索引。
使用正确的类型
不要将日期和时间存储为单独的字符串,而是将它们存储为单个 datetime 或单独的 date 和 time 类型。有关使用哪一个的更多信息,请参阅下面的索引。这既更紧凑,允许索引,更快的排序,而且无需进行转换即可使用date and time functions。
同样,请务必使用适当的numeric type 作为纬度和经度。您可能需要使用numeric 来确保精度。
由于您要存储数十亿行,因此请务必使用 bigint 作为主键。一个普通的 int 最多只能达到 20 亿。
将重复的数据移到另一个表中。
不要在每一行中存储有关设备的信息,而是将其存储在单独的表中。然后只将设备的 ID 存储在您的日志中。这将减少您的存储大小,并消除由于数据重复而导致的错误。请务必将设备 ID 声明为外键,这将提供 referential integrity 和索引。
添加索引
Indexes 允许数据库非常、非常高效地搜索数百万或数十亿行。确保您经常使用的行上有索引,例如您的时间戳。
date 和 deviceID 上缺少索引可能是您的查询如此缓慢的原因。如果没有索引,MySQL 必须查看数据库中称为full table scan 的每一行。这就是为什么您的查询如此缓慢,缺少索引的原因。
您可以通过explain 发现您的查询是否使用索引。
datetime 或 time + date?
通常最好将您的日期和时间存储在单个列中,通常称为created_at。然后你可以使用date 来获取日期部分,就像这样。
select *
from gps_logs
where date(created_at) = '2018-07-14'
有问题。问题是索引如何工作......或不工作。由于函数调用,where date(created_at) = '2018-07-14' 不会使用索引。 MySQL 将在每一行上运行date(created_at)。这意味着性能会扼杀全表扫描。
您可以通过仅使用 datetime 列来解决此问题。这将使用索引并且效率很高。
select *
from gps_logs
where '2018-07-14 00:00:00' <= created_at and created_at < '2018-07-15 00:00:00'
或者您可以将单个 datetime 列拆分为 date 和 time 列,但这会带来新问题。查询跨日边界的范围变得困难。就像也许你想在不同的时区度过一天。单列很容易。
select *
from gps_logs
where '2018-07-12 10:00:00' <= created_at and created_at < '2018-07-13 10:00:00'
但它更多地涉及单独的date 和time。
select *
from gps_logs
where (created_date = '2018-07-12' and created_time >= '10:00:00')
or (created_date = '2018-07-13' and created_time < '10:00:00');
或者您可以使用partial indexes like Postgresql 切换到数据库。部分索引允许您仅索引值的一部分或函数的结果。而且 Postgresql 在很多事情上都比 MySQL 做得更好。这是我推荐的。
尽可能多地使用 SQL。
例如,如果您想知道每台设备每天有多少日志条目,而不是拉出所有行并自己计算它们,您可以使用group by 按设备和日期对它们进行分组。
select gps_device_id, count(id) as num_entries, created_at::date as day
from gps_logs
group by gps_device_id, day;
gps_device_id | num_entries | day
---------------+-------------+------------
1 | 29310 | 2018-07-12
2 | 23923 | 2018-07-11
2 | 23988 | 2018-07-12
有了这么多数据,您将需要严重依赖 group by 和关联的 aggregate functions,例如 sum、count、max、min 等等。
避免select *
如果您必须检索 86400 行,那么从数据库中获取所有数据的成本可能会很高。您可以通过仅获取所需的列来显着加快速度。这意味着使用select only, the, specific, columns, you, need 而不是select *。
把它们放在一起。
在 PostgreSQL 中
您在 PostgreSQL 中的架构应该如下所示。
create table gps_devices (
id serial primary key,
name text not null
-- any other columns about the devices
);
create table gps_logs (
id bigserial primary key,
gps_device_id int references gps_devices(id),
created_at timestamp not null default current_timestamp,
latitude numeric(12,9) not null,
longitude numeric(12,9) not null
);
create index timestamp_and_device on gps_logs(created_at, gps_device_id);
create index date_and_device on gps_logs((created_at::date), gps_device_id);
一个查询通常每个表只能使用一个索引。由于您将大量搜索时间戳和设备 ID,timestamp_and_device 结合了时间戳和设备 ID 的索引。
date_and_device 是一回事,但它只是时间戳的日期部分的部分索引。这将使where created_at::date = '2018-07-12' and gps_device_id = 42 非常高效。
在 MySQL 中
create table gps_devices (
id int primary key auto_increment,
name text not null
-- any other columns about the devices
);
create table gps_logs (
id bigint primary key auto_increment,
gps_device_id int references gps_devices(id),
foreign key (gps_device_id) references gps_devices(id),
created_at timestamp not null default current_timestamp,
latitude numeric(12,9) not null,
longitude numeric(12,9) not null
);
create index timestamp_and_device on gps_logs(created_at, gps_device_id);
非常相似,但没有部分索引。因此,您要么需要始终在 where 子句中使用裸 created_at,要么切换到单独的 date 和 time 类型。