2

上一张表,这些数据存储在接近 3-4gb 中,但数据在存储之前/之后没有被压缩。我不是 DBA,所以我对一个好的策略有点不知所措。

该表用于记录对我的应用程序(用户配置文件)中特定模型的更改,但有一个棘手的要求:我们应该能够在任何给定日期获取配置文件的状态。

数据(单表):

id, username, email, first_name, last_name, website, avatar_url, address, city, zip, phone

只有两个要求:

  1. 能够获取给定模型的更改列表
  2. 能够在给定日期获取模型的状态

以前,所有配置文件数据都是针对单个更改存储的,即使只更改了一列。但是获取特定日期的“快照”很容易。

我优化数据结构的前几个解决方案:

(1) 仅存储更改的列。这将大大减少存储的数据,但会使获取数据快照变得相当复杂。我必须合并到给定日期(可能是数千个)之前的所有更改,然后将其应用于模型。但该模型不可能是新模型(仅存储更改的数据)。为此,我必须首先复制当前profiles表中的所有数据,然后获取快照,将更改应用于这些基本模型。

(2) 存储全部数据,但转换为压缩格式,如 gzip 或二进制或诸如此类。这将删除查询数据而不是获取更改的能力。例如,我无法获取所有更改where email = ''。我基本上会有一个包含转换数据的列,存储整个配置文件。

然后,我想使用相关的 MySQL 表选项,如 ARCHIVE 来进一步减少空间。

所以我的问题是,有没有比上面的 1/2 更好的方法,如果没有,哪个更好?

4

5 回答 5

4

首先,我一点也不担心 3GB 表(除非它在很短的时间内增长到这个大小)。MySQL 可以接受。空间不应该是一个问题,请记住,一个 500 GB 的硬盘大约需要 4 个工时(在我的国家)。

话虽如此,为了降低您的存储要求,请为您要监控的表的每个字段创建一个表。假设这样的profile表:

CREATE TABLE profile (
    profile_id INT PRIMARY KEY,
    username VARCHAR(50),
    email VARCHAR(50) -- and so on
);

...创建两个历史表:

CREATE TABLE profile_history_username (
    profile_id INT NOT NULL,
    username VARCHAR(50) NOT NULL, -- same type as profile.username
    changedAt DATETIME NOT NULL,
    PRIMARY KEY (profile_id, changedAt),
    CONSTRAINT profile_id_username_fk
        FOREIGN KEY profile_id_fkx (profile_id)
        REFERENCES profile(profile_id)
);

CREATE TABLE profile_history_email (
    profile_id INT NOT NULL,
    email VARCHAR(50) NOT NULL, -- same type as profile.email
    changedAt DATETIME NOT NULL,
    PRIMARY KEY (profile_id, changedAt),
    CONSTRAINT profile_id_fk
        FOREIGN KEY profile_id_email_fkx (profile_id)
        REFERENCES profile(profile_id)
);

每次更改 中的一个或多个字段时profile,请在每个相关历史记录表中记录更改:

START TRANSACTION;

-- lock all tables
SELECT @now := NOW()
FROM profile
JOIN profile_history_email USING (profile_id)
WHERE profile_id = [a profile_id]
FOR UPDATE;

-- update main table, log change
UPDATE profile SET email = [new email] WHERE profile_id = [a profile_id];
INSERT INTO profile_history_email VALUES ([a profile_id], [new email], @now);

COMMIT;

您可能还想设置适当的AFTER触发器,profile以便自动填充历史记录表。

检索历史信息应该很简单。为了在给定时间点获取配置文件的状态,请使用以下查询:

SELECT
    (
        SELECT username FROM profile_history_username
        WHERE profile_id = [a profile_id] AND changedAt = (
            SELECT MAX(changedAt) FROM profile_history_username
            WHERE profile_id = [a profile_id] AND changedAt <= [snapshot date]
        )
    ) AS username,

    (
        SELECT email FROM profile_history_email
        WHERE profile_id = [a profile_id] AND changedAt = (
            SELECT MAX(changedAt) FROM profile_history_email
            WHERE profile_id = [a profile_id] AND changedAt <= [snapshot date]
        )
    ) AS email;
于 2013-07-12T10:14:30.877 回答
1

您需要一个缓慢变化的维度:

我只会为电子邮件和电话执行此操作,以便您理解(注意我使用两个键的事实,一个在表中是唯一的,另一个是它所关注的用户唯一的。这就是表key 标识记录,user key 标识用户):

table_id、user_id、电子邮件、电话、created_at、inactive_at、is_current

  • 1, 1, mario@yahoo.it, 123456, 2012-01-02, , 2013-04-01, 没有
  • 2, 2, erik@telecom.de, 123457, 2012-01-03, 2013-02-28, 否
  • 3, 3, vanessa@o2.de, 1234568, 2012-01-03, null, 是
  • 4, 2, erik@telecom.de, 123459, 2012-02-28, null, 是
  • 5, 1, super.mario@yahoo.it, 654321,2013-04-01, 2013-04-02, 没有
  • 6, 1, super.mario@yahoo.it, 123456,2013-04-02, null, 是

数据库的最新状态

select * from FooTable where inactive_at is null

或者

select * from FooTable where is_current = 'yes'

mario 的所有更改(mario 为 user_id 1)

select * from FooTable where user_id = 1;

2013 年 1 月 1 日至 2013 年 5 月 1 日之间的所有更改

select * from FooTable where created_at between '2013-01-01' and '2013-05-01';

并且您需要与旧版本进行比较(借助存储过程、java 或 php 代码...您选择)

select * from FooTable where incative_at between '2013-01-01' and '2013-05-01';

如果你愿意,你可以做一个花哨的 sql 语句

select f1.table_id, f1.user_id, 
  case when f1.email = f2.email then 'NO_CHANGE' else concat(f1.email , ' -> ',  f2.email) end,
  case when f1.phone = f2.phone then 'NO_CHANGE' else concat(f1.phone , ' -> ',  f2.phone) end
  from FooTable f1 inner join FooTable f2 
on(f1.user_id = f2.user_id)
where f2.created_at in 
   (select max(f3.created_at) from Footable f3 where f3.user_id = f1.user_id 
      and f3.created_at < f1.created_at and f1.user_id=f3.user_id) 
 and f1.created_at between '2013-01-01' and '2013-05-01' ;

正如您所看到的多汁查询,将 user_ 与预览用户行进行比较...


2013-03-01 数据库的状态

select * from FooTable where table_id in
   (select max(table_id) from FooTable where inactive_at <= '2013-03-01'  group by user_id 
     union
    select id from FooTable where inactive_at is null group by user_id having count(table_id) =1 );

我认为这是实现您想要的最简单的方法...您可以实现数百万个表的关系模型,但是查询它会很痛苦


您的数据库不够大,我每天都在使用更大的数据库。现在告诉我,你在新服务器上节省的钱是否值得你花时间在超级复杂的关系模型上?

顺便说一句,如果数据变化太快,这种方法就不能用了……


奖金:优化:

  • 在 created_at、inactive_at、user_id 和该对上创建索引

  • 执行分区(水平和垂直)

于 2013-07-08T18:58:08.840 回答
1

我将提供另一种解决方案,只是为了多样化。

架构

PROFILE
    id INT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE

PROFILE_ATTRIBUTE
    id INT PRIMARY KEY,
    profile_id INT NOT NULL FOREIGN KEY REFERENCES PROFILE (id),
    attribute_name VARCHAR(50) NOT NULL,
    attribute_value VARCHAR(255) NULL,
    created_at DATETIME NOT NULL DEFAULT GETTIME(),
    replaced_at DATETIME NULL

对于您正在跟踪的所有属性,只需PROFILE_ATTRIBUTE在更新记录时添加记录,并用它被替换的 DATETIME 标记先前的属性记录。

选择当前配置文件

SELECT *
FROM PROFILE p
    LEFT JOIN PROFILE_ATTRIBUTE pa
    ON p.id = pa.profile_id
WHERE p.username = 'username'
    AND pa.replaced_at IS NULL

在日期选择个人资料

SELECT *
FROM PROFILE p
    LEFT JOIN PROFIILE_ATTRIBUTE pa
    ON p.id = pa.profile_id
WHERE p.username = 'username'
    AND pa.created_at < '2013-07-01'
    AND '2013-07-01' <= IFNULL(pa.replaced_at, GETTIME())

更新属性时

  • 插入新属性
  • 更新前一个属性的replaced_at

新属性的与对应的旧属性created_at匹配可能很重要。replaced_at这将使得给定属性名称的属性值有一个完整的时间线。

优点

  • 简单的两表架构(我个人不喜欢每字段表的方法)
  • 可以在不更改架构的情况下添加其他属性
  • 假设应用程序位于该数据库之上,轻松映射到 ORM 系统
  • 可以很容易地看到attribute_name一段时间内的历史。

缺点

  • 不强制执行完整性。例如,模式不限制replaced_at具有相同的多个 NULL 记录attribute_name......也许这可以通过两列 UNIQUE 约束来强制执行
  • 假设您将来添加一个新字段。现有配置文件不会为新字段选择一个值,直到它们保存一个值。如果它是一列,这与返回为 NULL 的值相反。这可能是也可能不是问题。

如果您使用这种方法,请确保您在created_atreplaced_at列上有索引。

可能还有其他优点或缺点。如果评论者有意见,我会用更多信息更新这个答案。

于 2013-07-19T00:06:58.677 回答
1

您无法压缩数据而不必解压缩以进行搜索 - 这将严重损害性能。如果数据确实经常变化(即每条记录平均超过 20 次),那么存储和检索将其结构化为一系列变化会更有效:

考虑:

 CREATE TABLE profile (
   id INT NOT NULL autoincrement,
   PRIMARY KEY (id);
 );
 CREATE TABLE profile_data (
   profile_id INT NOT NULL,
   attr ENUM('username', 'email', 'first_name'
        , 'last_name', 'website', 'avatar_url'
        , 'address', 'city', 'zip', 'phone') NOT NULL,
   value CARCHAR(255),
   starttime DATETIME DEFAULT CURRENT_TIME,
   endtime DATETIME,
   PRIMARY KEY (profile_id, attr, starttime)
   INDEX(profile_id),
   FOREIGN KEY (profile_id) REFERENCES profile(id)
 );

当您为现有记录添加新值时,请在屏蔽记录中设置结束时间。然后获取日期 $T 的值:

 SELECT p.id, attr, value
 FROM profile p
 INNER JOIN profile_date d
 ON p.id=d.profile_id
 WHERE $T>=starttime
 AND $T<=IF(endtime IS NULL,$T, endtime);

或者只是有一个开始时间,并且:

SELECT p.id, attr, value
 FROM profile p
 INNER JOIN profile_date d
 ON p.id=d.profile_id
 WHERE $T>=starttime
 AND NOT EXISTS (SELECT 1
   FROM prodile_data d2
   WHERE d2.profile_id=d.profile_id
   AND d2.attr=d.attr
   AND d2.starttime>d.starttime
   AND d2.starttime>$T);

(使用 MAX concat 技巧会更快)。

但是,如果数据没有以该频率变化,则将其保留在当前结构中。

于 2013-07-08T21:19:17.563 回答
1

如果您尝试将所有发生的更改放在不同的表中,然后如果您在某个日期需要一个实例,您可以将它们加入并通过比较日期来显示,例如,如果您想要一个在 7 月 1 日的实例,您可以运行带有条件的查询date 等于或小于 7 月 1 日,并以 asc 排序将计数限制为 1。这样,连接将准确生成 7 月 1 日的实例。通过这种方式,您甚至可以找出最频繁更新的模块。另外,如果您想保持所有数据平坦,请尝试基于月份进行范围分区,这样 mysql 将很容易处理它。

注意:按日期我的意思是存储日期的 unix 时间戳,比较容易比较。

于 2013-07-17T07:34:11.963 回答