拥有数百万行的数据库表

8

例如,我有一些GPS设备,它们每秒向我的数据库发送信息。

因此,一个设备在MySQL数据库中创建1行,包含以下8个列:

id=12341 date=22.02.2018 time=22:40 langitude=22.236558789 longitude=78.9654582 deviceID=24 name=device-name someinfo=asdadadasd

因此,每分钟会创建60行,每天24小时会创建864000行,而对于一个月(31天)则会创建2678400行

因此,一个设备每月在我的数据库表中创建260万行记录(每月删除记录)。如果有更多的设备,则会创建2.6百万*设备数量的行。

我的问题如下:

问题 1: 如果我从 PHP 中进行搜索(仅限当前日期和一个设备),该怎么做?

SELECT * FROM TABLE WHERE date='22.02.2018' AND deviceID= '24'

最大可能的结果将会有86400行。
这会不会对我的服务器造成太大负担?

问题2:限制为5小时(18000行)是否会对数据库或服务器造成问题,或者比第一个例子还要少?

  SELECT * FROM TABLE WHERE date='22.02.2018' AND deviceID= '24' LIMIT 18000

问题 3:如果我只从数据库中显示一个结果,会不会使服务器过载?


 SELECT * FROM TABLE WHERE date='22.02.2018' AND deviceID= '24' LIMIT 1

这是不是意味着,如果我有数百万行数据,那么加载其中的1000行和只显示1个结果会对服务器造成相同的负载?


你好,欢迎来到Stack Overflow。如果我们知道你在使用这些查询做什么,那么回答你的问题会更有帮助。对于你的第一个查询,我怀疑你不想看到一整天的864,000秒。我猜你会在PHP中进行一些处理。也有可能你可以在MySQL中进行这些处理,这通常更加高效。 - Schwern
顺便提一下,864000秒是10天; p - Lawrence Cherone
300万行并不算太多,30,000万行也是如此。这取决于你使用它们的目的。你是每天、每小时、每分钟查询100k行吗?请解释一下你的用例。 - The Impaler
4个回答

20

如果你有一个良好的设计模式和良好的索引,SQL数据库可以轻松处理数百万行数据。

使用适当的类型

将日期和时间存储为单个datetime或分别为datetime类型而不是将它们存储为单独的字符串。有关要使用哪种类型的更多信息,请参见下面的索引。这样可以使存储更紧凑,允许进行索引,排序更快,并且可以使用日期和时间函数而无需进行转换。

同样地,请确保对纬度和经度使用适当的数字类型。您可能想使用numeric以确保精度。

由于您将要存储数十亿行记录,请确保在主键上使用bigint。常规的int只能达到约20亿。

将重复数据移到另一个表中

将有关设备的信息存储在单独的表中,而不是在每一行中存储该信息。然后仅在您的日志中存储设备ID。这将减小存储大小,并消除由于数据重复而导致的错误。请确保将设备ID声明为外键,这将提供引用完整性和索引。

添加索引

索引是允许数据库非常高效地搜索数百万或数十亿行数据的方式。请确保在频繁使用的行上创建索引,例如时间戳。

datedeviceID上缺少索引可能是您的查询非常缓慢的原因。没有索引,MySQL必须查看数据库中的每一行,这称为全表扫描。这就是为什么你的查询很慢,因为你缺少索引。

您可以使用explain来发现您的查询是否使用索引。

datetimetime+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 列分成 datetime 两列,但这会引入新的问题。查询跨越一天边界的范围变得困难,比如说你想要一个不同时区的日期。使用单个列更加简单。

select *
from gps_logs
where '2018-07-12 10:00:00' <= created_at and created_at < '2018-07-13 10:00:00'

但涉及到单独的日期时间会更加复杂。

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');

你可以切换到支持部分索引的数据库,如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 和相关的聚合函数,例如 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,要么切换到单独的datetime类型。


如果他/她一次只查询一个设备,可能索引列应该是 gps_device_is,然后是 created_at - The Impaler
@TheImpaler 我的方法涵盖了所有情况。如果他们只查询 where gps_device_id = ?,它将使用外键索引。如果他们查询 where created_at = ?,它将使用 timestamp_and_device,因为 created_at 是第一个。如果他们同时查询两个条件,如 where created_at = ? and gps_device_id = ?,它将使用 timestamp_and_device - Schwern
你说得完全正确。我完全忘记了MySQL会在没有询问的情况下为外键创建索引。 - The Impaler

1

我刚刚看到你的问题,对于我来说答案是:

只需创建一个独立的纬度和经度表格,将您的 ID 作为外键并保存在那里即可。


1

不知道你想运行的确切查询,我只能猜测最佳结构。话虽如此,你应该以使用每行最少字节数的最优类型为目标。这样可以加快查询速度。

例如,你可以使用以下结构:

create table device (
  id int primary key not null,
  name varchar(20),
  someinfo varchar(100)
);

create table location (
  device_id int not null,
  recorded_at timestamp not null,
  latitude double not null, -- instead of varchar; maybe float?
  longitude double not null, -- instead of varchar; maybe float?
  foreign key (device_id) references device (id)
);

create index ix_loc_dev on location (device_id, recorded_at);

如果您提供准确的查询(命名列),我们可以为其创建更好的索引。
由于您的查询选择性可能不佳,您的查询可能会运行全表扫描。对于这种情况,我采用了最小可能的数据类型来加快速度:
create table location (
  device_id tinyint not null,
  recorded_at timestamp not null,
  latitude float not null,
  longitude float not null,
  foreign key (device_id) references device (id)
);

真的想不出比这更小的东西了。


请注意,对recorded_at进行索引无法帮助处理像date(recorded_at) = '2017-01-02'这样的查询。 - Schwern
是的,表达式应该在运算符的右侧。也许只需使用 between - The Impaler
现在我意识到这些查询很可能根本不会使用任何索引。 - The Impaler

0
我能给你的最好建议是使用时间序列数据库来存储和访问时间序列数据。你可以在本地托管任何类型的时间序列数据库引擎,只需在开发其访问方法时投入更多资源,或者使用专门用于遥测数据的数据库,比如this

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接