最佳数据库表优化方法

3
我有一个数据库表格,它已经变得非常大(几亿行),需要优化,但在我开始分区之前,我想询问一下建议。
以下是用法:
0. 表格包含约10个长度为20字节的列。 1. 每秒执行数百次插入操作。 2. 每小时基于列'a'执行几次选择语句(其中a='xxxx')。 3. 基于日期列执行删除语句。(删除超过1年的日期)通常每天执行一次。
关键要求是加速插入和选择语句,并能够在删除时保留1年的历史数据而不锁定整个表格。
我猜必须有两个索引,一个用于列'a',另一个用于日期字段。或者是两者都可以优化?
在选择速度和删除速度之间会有必要的权衡吗?
分区是唯一的解决方案吗?对于这样的表格,有什么好的分区策略?
我正在使用PostgreSQL 8.4数据库。
5个回答

4
与其将它保留为单个物理表,不如考虑一下PostgreSQL分区?从版本8.1开始就支持它。
分区可以帮助您避免选择快速插入或快速删除性能之间的问题。您始终可以按年/月对表进行分区,并仅删除不再需要的分区。删除分区非常快,并且插入到小分区也非常快。
来自手册:
分区是指将逻辑上的一个大表拆分为较小的物理片段。分区可以提供几个好处:
- 对于某些类型的查询,查询性能可以显著提高。 - 更新性能也可以得到改善,因为表的每个部分都具有比整个数据集上的索引更小的索引。当索引不再容易适合内存时,索引上的读取和写入操作都需要越来越多的磁盘访问。 - 如果计划在分区设计中,则可以通过简单地删除其中一个分区来完成批量删除。DROP TABLE比批量DELETE要快得多,更不用说随之而来的VACUUM开销了。 - 很少使用的数据可以迁移到更便宜和更慢的存储介质中。
通常只有当表非常大时才会获得收益。表从分区中受益的确切点取决于应用程序,尽管一个经验法则是表的大小应超过数据库服务器的物理内存。
目前,PostgreSQL通过表继承支持分区。每个分区必须创建为单个父表的子表。父表本身通常为空;它存在只是为了表示整个数据集。在尝试实现分区之前,您应该熟悉继承(请参见第5.8节)。

但是按年/月对表进行分区可以优化删除操作,而不是插入/选择操作,对吧? - Pat
@Pat:它应该同时优化。拥有小分区可以提高插入效率,如果大多数查询只需要“当前”数据,那么这些查询也将得到优化。至于针对整个表(所有分区)进行操作的选择操作,它们已经产生了高成本,物理分区应该是可以忽略的。 - LBushkin
所以如果我理解正确,您建议的分区将优化删除和插入,它不会使选择更糟,因为它已经很糟了。但要求是加快“插入和选择”而不是加快“删除和插入”。或者我在您的回答中漏掉了什么? - Pat

3
分区是解决方案,正如其他人所说,但是:
我会在某些哈希(a)上进行分区。如果a是整数,则a%256很好。如果它是文本,则可以使用类似于substring(md5(a)for 2)的内容。
这将加快插入和选择的速度。
对于删除操作,我会更频繁地运行它们,但是规模较小并且也进行了分区。我会在每小时(在XX:30)运行它们,并像这样:
delete from table_name
where date<(current_date - interval '1 year')
and
  hash(a)
  =
  (extract(doy from current_timestamp) * 24
    + extract(hour from current_timestamp))::int % 256;

编辑:我刚刚测试过这个:
create function hash(a text) returns text as $$ select substring(md5($1) for 1) $$ language sql immutable strict;
CREATE TABLE tablename (id text, mdate date);
CREATE TABLE tablename_partition_0 ( CHECK ( hash(id) = '0' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_1 ( CHECK ( hash(id) = '1' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_2 ( CHECK ( hash(id) = '2' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_3 ( CHECK ( hash(id) = '3' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_4 ( CHECK ( hash(id) = '4' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_5 ( CHECK ( hash(id) = '5' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_6 ( CHECK ( hash(id) = '6' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_7 ( CHECK ( hash(id) = '7' ) ) INHERITS (tablename); 
CREATE TABLE tablename_partition_8 ( CHECK ( hash(id) = '8' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_9 ( CHECK ( hash(id) = '9' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_a ( CHECK ( hash(id) = 'a' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_b ( CHECK ( hash(id) = 'b' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_c ( CHECK ( hash(id) = 'c' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_d ( CHECK ( hash(id) = 'd' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_e ( CHECK ( hash(id) = 'e' ) ) INHERITS (tablename);
CREATE TABLE tablename_partition_f ( CHECK ( hash(id) = 'f' ) ) INHERITS (tablename);
analyze;
explain select * from tablename where id='bar' and hash(id)=hash('bar');

查询计划如下: - 结果 (cost=0.00..69.20 rows=2 width=36) -> 追加 (cost=0.00..69.20 rows=2 width=36) -> 顺序扫描 tablename (cost=0.00..34.60 rows=1 width=36) 过滤器: ((id = 'bar'::text) AND ("substring"(md5(id), 1, 1) = '3'::text)) -> 顺序扫描 tablename_partition_3 tablename (cost=0.00..34.60 rows=1 width=36) 过滤器: ((id = 'bar'::text) AND ("substring"(md5(id), 1, 1) = '3'::text)) (6 行)
您需要在查询中添加 hash(id)=hash('searched_value'),否则Postgres会搜索所有表。
编辑:您还可以使用规则系统自动插入到正确的表中。
create rule tablename_rule_0 as
  on insert to tablename where hash(NEW.id)='0'
  do instead insert into tablename_partition_0 values (NEW.*);
create rule tablename_rule_1 as
  on insert to tablename where hash(NEW.id)='1'
  do instead insert into tablename_partition_1 values (NEW.*);
-- and so on
insert into tablename (id) values ('a');
select * from tablename_partition_0;
 id | mdate 
----+-------
 a  | 
(1 row)

PostgreSQL支持哈希分区吗? - Pat
有点儿。您需要在查询中添加显式的hash(id)=hash(...)。 - Tometzky

0

一种解决方案是基于插入日期进行分区。

也就是说,您的应用程序(或DAO)根据某些逻辑(结合当前日期(或上一个分区切片开始后的时间)和/或“上一个”分区的大小)决定要插入哪个表。或将这样的逻辑卸载到每日脚本中,并让脚本为DAO的使用填充一些“这是要使用的分区”。

这立即消除了您删除“旧”行的需求(只需删除旧分区);它还确保您的插入周期性地开始填充小表,其中包括加速“平均”INSERT / SELECT速度的事情(最坏情况仍然同样缓慢)


这对于删除操作来说很好,但根据问题描述“关键要求是加快插入和查询语句”的要求,在这种情况下,在列a上进行分区是否更好? - Pat

0
如果您将此表分成适当的分区,您就可以使用截断而不是删除,这将降低维护成本,因为它不会创建死空间。

这对于删除操作来说很好,但根据问题描述“关键要求是加速INSERT和SELECT语句”,在这种情况下,在列a上进行分区是否更好? - Pat
清理删除过程也有助于SELECT(而不是INSERT),因为它会减少由于膨胀而导致的硬件随机读取。是的,适当的分区绝对是整体解决方案。 - Joshua D. Drake

0

我不是专家,但似乎在列“a”上进行分区会加快您的选择,但在日期上进行分区(正如所有其他答案所建议的那样)会加快删除(删除表),但对于您的选择无用。

看起来,这两种情况都会增强插入性能。

有没有专家关注这个问题? 在两个字段上进行分区是否可能/有用?


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