2

このデータが保存された前のテーブルは 3 ~ 4 GB に近づきましたが、データは保存の前後に圧縮されていませんでした。私は DBA ではないので、適切な戦略については少し詳しくありません。

テーブルは、アプリケーション (ユーザー プロファイル) の特定のモデルへの変更をログに記録するためのものですが、トリッキーな要件が 1 つあります。それは、任意の日付でプロファイルの状態を取得できる必要があるということです。

データ (単一テーブル):

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

要件は次の 2 つだけです。

  1. 特定のモデルの変更のリストを取得できる
  2. 特定の日付のモデルの状態を取得できる

以前は、1 つの列だけが変更された場合でも、すべてのプロファイル データが 1つの変更に対して保存されていました。しかし、特定の日付の「スナップショット」を取得するのは簡単でした。

データ構造を最適化するための私の最初のいくつかのソリューション:

(1) 変更された列のみを保存します。これにより、保存されるデータが大幅に削減されますが、データのスナップショットを取得することが非常に複雑になります。特定の日付 (数千になる可能性があります) までのすべての変更をマージしてから、それをモデルに適用する必要があります。しかし、そのモデルは新しいモデルではありません (変更されたデータのみが保存されます)。profilesこれを行うには、最初に現在のテーブルからすべてのデータをコピーし、次にスナップショットを取得してそれらの基本モデルに変更を適用する必要があります。

(2) データ全体を保存しますが、gzip やバイナリなどの圧縮形式に変換します。これにより、変更を取得する以外にデータをクエリする機能が削除されます。たとえば、すべての変更を取得できませんでしwhere email = ''た。基本的に、変換されたデータを含む単一の列を持ち、プロファイル全体を格納します。

次に、ARCHIVE などの関連する MySQL テーブル オプションを使用して、スペースをさらに削減したいと考えています。

私の質問は、上記の 1/2 よりも優れたアプローチであると思われる他のオプションはありますか?

4

5 に答える 5

4

まず、3 GB のテーブルについてはまったく心配しません (非常に短期間でこのサイズに大きくならない限り)。MySQLはそれを取ることができます。容量は気にする必要はありません。500 GB のハードディスクのコストは約 4 工数 (私の国では) であることを覚えておいてください。

そうは言っても、ストレージ要件を下げるために、監視するテーブルのフィールドごとに 1 つのテーブルを作成します。次のようなテーブルを想定しprofileます。

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

... 2 つの履歴テーブルを作成します。

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

で 1 つまたは複数のフィールドを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

多様性のためだけに、もう 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の が対応する古い属性の と一致することがおそらく重要です。これは、特定の属性名の属性値の途切れのないタイムラインが存在するようにするためです。

利点

  • シンプルな 2 テーブル アーキテクチャ (個人的にはフィールドごとのテーブル アプローチは好きではありません)
  • スキーマを変更せずに属性を追加できます
  • アプリケーションがこのデータベース上にあると仮定すると、ORM システムに簡単にマッピングできます。
  • 特定の期間の履歴を簡単に確認できますattribute_name

短所

  • 整合性は強制されません。たとえば、スキーマはreplaced_at同じ複数の NULL レコードを制限しませんattribute_name...おそらくこれは、2 列の UNIQUE 制約で強制できます
  • 将来、新しいフィールドを追加するとします。既存のプロファイルは、値を保存するまで、新しいフィールドの値を選択しません。これは、値が列の場合に NULL として返されるのとは対照的です。これは問題になる場合とそうでない場合があります。

このアプローチを使用する場合は、created_atおよびreplaced_at列にインデックスがあることを確認してください。

他にもメリットやデメリットがあるかもしれません。コメンターが入力した場合は、この回答を更新して詳細情報を提供します。

于 2013-07-19T00:06:58.677 に答える
1

発生したすべての変更を別のテーブルに入れようとする場合、後である日付にインスタンスが必要な場合は、それらを結合して日付を比較して表示します。たとえば、7 月 1 日にインスタンスが必要な場合は、次の条件でクエリを実行できます。日付が 7 月 1 日以下で、カウントを 1 に制限して昇順で並べ替えます。これにより、結合によって 7 月 1 日のインスタンスが正確に生成されます。このようにして、最も頻繁に更新されるモジュールを把握することさえできます。また、すべてのデータをフラットに保ちたい場合は、mysql が非常に簡単に処理できるように、月に基づいて範囲分割を試してください。

注:日付とは、日付のUNIXタイムスタンプを保存することを意味し、比較がかなり簡単です。

于 2013-07-17T07:34:11.963 に答える
1

変化の遅いディメンションが必要です。

これは電子メールと電話でのみ行うので、ご理解ください (2 つのキーを使用することに注意してください。1 つはテーブル内で一意であり、もう 1 つは関係するユーザーに固有です。これはテーブルです。キーはレコードを識別し、ユーザー キーはユーザーを識別します):

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'

マリオへのすべての変更 (マリオは 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_with をプレビューのユーザー行と比較するには...


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

検索するために圧縮を解除しないと、データを圧縮することはできません。これにより、パフォーマンスが大幅に低下します。データが実際にそれほど頻繁に変更されている場合 (つまり、レコードごとに平均 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 に答える