如何在 Rails 中使用 PostgresSQL 表分区

当你在开发中遇到这样一种场景,需要收集单个设备的数据信息,这个这个设备每隔 10 秒就会发一次数据 到 Rails 服务器,服务器需要把每次来的数据收集起来,以便以后数据分析使用,但这个设备会很多,几千, 几万,甚至几十万都是有可能的。

那一个设备的一天的数据量就是 10 * 60 * 60 * 24 = 864000 条,一年的就是 3 个亿,如果 1 万个设备的话, 就是 1 万亿条数据,看起来数据量有点恐怖,如果放在单表里面,势必随着数据量的增大,查询,插入等数据操作 的性能都会受到很大影响。

这个时候,有什么好的方法可以解决这个问题呢? 我当时正好使用的是 PostgresSQL,于是查了些资料,很多人 推荐表分区来解决此问题,那让我们来看看如何使用这一特性吧。

什么是表分区

在 PG 里面就是一个主表,会有很多的子表继承主表,字段和主表一样。一般都是建立一个主表,里面是空,然后每个分区都去继承它。无论何时,都应保证主表里面是空的。

PostgresSQL 分区是把逻辑上的一个大表分割成物理上的几块。分区不仅能带来性能的提升,还能带来管理和维护上的方便。

分区的好处是:

  • 查询性能可以得到极大提升。
  • 更新的性能也可以得到提升,因为表的每块的索引要比在整个数据集上的索引要小。如果索引不能全部放在内存里,那么在索引上的读和写都会产生更多的磁盘访问。
  • 批量删除可以用简单的删除某个分区来实现。
  • 可以将很少用的数据移动到便宜的、转速慢的存储介质上。

表在多大情况下才考虑分区呢? PostgresSQL官方给出的建议是:当表本身大小超过了机器物理内存的实际大小时(the size of the table should exceed the physical memory of the database server),可以考虑分区。

Rails 中如何实现分区

Gemfile

因为本人使用的是 Rails 4.2 所以 Gemfile 配置如下, partitioned 是分区的主 gem, activerecord-redshift-adapter 是 partitioned 依赖的 gem ,为了兼容 rails 4 设置的版本。

1
2
gem "partitioned", github: "dkhofer/partitioned", branch: "rails-4-2"
gem 'activerecord-redshift-adapter',  github: "arp/activerecord-redshift-adapter", branch: "rails4-compatibility"

创建分区规则

因为我需要以 created_at 字段按月分区,所以需要继承 Partitioned::ByMonthlyTimeField 类。

1
2
3
4
5
6
7
8
9
10
11
class PartitionedByCreatedAtMonthly < Partitioned::ByMonthlyTimeField
  self.abstract_class = true

  def self.partition_time_field
    :created_at
  end

  partitioned do |partition|
    partition.index :id, unique: true
  end
end

指定表使用此分区规则

如果你想指定哪张表使用此分区规则,只需要这个表的 model 继承 PartitionedByCreatedAtMonthly, 而不是继承 ActiveRecord::Base。

1
2
class DeviceLog < PartitionedByCreatedAtMonthly
end

创建好 5 年的分区表

使用 migration 来提前创建分区表,这时候数据库会新建这样的分区表:"device_logs_partitions".“p201607”, “device_logs_partitions”.“p201608”, “device_logs_partitions”.“p201609”…

1
2
3
4
5
6
7
8
9
10
11
12
class CreatePartitionedTables < ActiveRecord::Migration
  def up
    # 创建分区表专用的名字为 device_logs_partitions 的模式
    DeviceLog.create_infrastructure
    dates = DeviceLog.partition_generate_range(Date.today, Date.today + 5.year)
    DeviceLog.create_new_partition_tables(dates)
  end

  def down
    DeviceLog.delete_infrastructure
  end
end

测试

上面的配置完成后,运行 rake db:migrate,使用 DeviceLog 模型创建一条记录,你会发现创建的数据会根据 created_at 的值自动创建到对应的分区表里面去。

1
2
### 假设是今天 20170731 号运行的
DeviceLog.create(:log => 'xxx')

会看到如下的插入 SQL

1
INSERT INTO device_logs_partitions.p201607 (log,...) values ('xxx',...;

如何查询分区表的数据呢

因为所有数据都到分区表了,所以 gem 的作者不建议直接使用 DeviceLog.find 方法,取而代之的是下面的查询方法, 这样的话只会在分区表里面查询。

1
2
3
DeviceLog.from_partition(today).find(1)
DeviceLog.find(:first, conditions: {created_at:
  @select_date.beginning_of_day..@select_date.end_of_day})

其他分区规则

至此基本分区表用法就是这样,这个 gem 其实还提供其他的分区规则,具体用法类似:

1
2
3
4
5
6
Partitioned::ById
Partitioned::ByForeignKey
Partitioned::ByDailyTimeField
Partitioned::ByMonthlyTimeField
Partitioned::ByWeeklyTimeField
Partitioned::ByYearlyTimeField

总结

这个 gem 确实比自己在 PG 里面设定分区规则要方便很多,有类似需求的可以尝试使用下。

评论