web-dev-qa-db-ja.com

パフォーマンスを失うことなくテーブルを2つに分割する方法は?

https://stackoverflow.com/a/174047/14731 によると、まれにしか必要とされない列を分割すると、キャッシュが解放され、一般的に使用される列をより速く取得できるようになります。

列がalways一緒に取得されるテーブルを持っていますが、設計上の理由からそれらを分割したいと思います(複数のテーブル間の重複を減らし、コードの再利用を改善します)。たとえば、同じアクセス許可スキームを使用するさまざまなテーブルがあります。各テーブルに権限列を追加する代わりに、外部キーを使用して別の権限スキーマテーブルを参照したいと思います。

MySQLに100万行を入力し、両方のバージョンに対してクエリを実行しましたが、JOINを使用したバージョンの方が約3倍遅いことを発見しました(0.9秒対2.9秒)。

これが私のテーブルです:

original
(
    id BIGINT NOT NULL,
    first BIGINT NOT NULL,
    second BIGINT NOT NULL,
    third BIGINT NOT NULL
);
part1
(
    id BIGINT NOT NULL,
    first BIGINT NOT NULL,
    second BIGINT NOT NULL,
    PRIMARY KEY(id)
);
part2
(
    link BIGINT NOT NULL,
    third BIGINT NOT NULL,
    FOREIGN KEY (link) REFERENCES part1(id)
);

これが私のクエリです:

SELECT first, second, third FROM original;
SELECT part1.first, part1.second, part2.third FROM part1, part2 WHERE part2.link = part1.id;

分割デザインのパフォーマンスオーバーヘッドを削減する方法はありますか?


このテストを自分の側で再現したい場合は、次のJavaアプリケーションを使用して、データベースに入力するSQLスクリプトを生成できます。

import Java.io.FileNotFoundException;
import Java.io.PrintWriter;

public class Main
{
    public static void main(String[] args) throws FileNotFoundException
    {
        final int COUNT = 1_000_000;
        try (PrintWriter out = new PrintWriter("/import.sql"))
        {
            for (int i = 0; i < COUNT; ++i)
                out.println("INSERT INTO original VALUES (" + i + ", " + i + ", 0);");
            out.println("INSERT INTO original VALUES (" + (COUNT - 2) + ", " + (COUNT - 1) +
                ", 1);");
            out.println();
            for (int i = 0; i < COUNT; ++i)
            {
                out.println("INSERT INTO part1 (first, second) VALUES (" + i + ", " + i + ");");
                out.println("INSERT INTO part2 VALUES (LAST_INSERT_ID(), 0);");
            }
            out.println("INSERT INTO part1 (first, second) VALUES (" + (COUNT - 2) + ", " +
                (COUNT - 1) + ");");
            out.println("INSERT INTO part2 VALUES (LAST_INSERT_ID(), 1);");
            out.println();
        }
    }
}
2
Gili

これが、限られた範囲で、パフォーマンステスト後に正規化を使用する理由です。正規化は、結合(ソート)を犠牲にして行われます。 5NFでのDWHの主な目的は、データを安全に保存することであり、すばやく取得することではありません。

代替案1マテリアライズドビューの概念があります:ハードドライブに保存されたビュー。 MySQLはそのままでは提供しませんが、この記事- MySQLでのマテリアライズドビュー -SPテーブルの更新/更新)を使用してこの機能を再作成する方法を説明します。

マテリアライズドビュー(MV)は、クエリの事前計算(マテリアライズド)結果です。単純なVIEWとは異なり、マテリアライズドビューの結果はどこかに、通常はテーブルに格納されます。マテリアライズドビューは、即時の応答が必要な場合や、マテリアライズドビューの基になるクエリが結果を生成するのに時間がかかる場合に使用されます。マテリアライズドビューはときどき更新する必要があります。これは、マテリアライズドビューが更新される頻度とそのコンテンツの実際の要件に依存します。基本的に、マテリアライズドビューはすぐに更新することも延期することもできます。完全に更新することも、特定の時点まで更新することもできます。 MySQLはそれ自体ではマテリアライズドビューを提供しません。

代替案2逆のことを行うことで、設計を実現することができます。メインテーブルを分割する代わりに、メインビューからのビューまたはテーブルを2〜3個作成します。このようにして、異なる値を持つスタースキーマの正規化されたテーブルを作成し、メインの高速テーブルを保持します。

パフォーマンスチューニングは常に、CPU(時間)、RAM、およびIO(スループットまたはスペース)の間のトレードオフです。この場合、CPUとIOの間です。

1
Stoleg

オプション#1:BIGINTの代わりにINT UNSIGNEDを使用する

フィールドが4,294,967,295を超えない場合は、INT UNSIGNEDに変更します

ALTER TABLE part1
    MODIFY COLUMN id     INT UNSIGNED NOT NULL AUTO_INCREMENT,
    MODIFY COLUMN first  INT UNSIGNED NOT NULL,
    MODIFY COLUMN second INT UNSIGNED NOT NULL;
ALTER TABLE part2
    MODIFY COLUMN link  INT UNSIGNED NOT NULL,
    MODIFY COLUMN third INT UNSIGNED NOT NULL;

データ型が小さいほど、特にJOINキーの場合、同じクエリが高速になります。

フィールドが16,777,215を超えない場合、さらに小さい列にはMEDIUMINT UNSIGNEDを使用します。

オプション#2:より大きな結合バッファーを使用する

これをmy.cnfに追加

[mysqld]
join_buffer_size = 16M

次に、MySQLにログインして実行します。

mysql> SET GLOBAL join_buffer_size = 1024 * 1024 * 16;

MySQLの再起動は必要ありません。

MySQLドキュメントを参照してください join_buffer_size

オプション#3:linkがインデックス化されていることを確認する

FOREIGN KEY参照があるため、これはかなり重要なポイントです。 FOREIGN KEYがない場合は、リンクがインデックス付けされていることを確認してください:

ALTER TABLE part2 ADD UNIQUE KEY (link);

試してみる !!!

UPDATE 2014-08-22 17:13 EDT

これを使用して、独自のバージョンのサンプルデータを作成しました。

DROP DATABASE IF EXISTS GILI; CREATE DATABASE GILI;
USE GILI
create table original 
(
    id mediumint unsigned not null auto_increment,
    first mediumint unsigned not null,
    second mediumint unsigned not null,
    third mediumint unsigned not null,
    primary key (id)
);
create table part1 like original;
alter table part1 drop column third;
create table part2 select third from original;
alter table part2 add link mediumint unsigned not null first;
alter table part2 add primary key (link);
insert into original (first,second,third) values (1,2,3);
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into original (first,second,third) select first,second,third from original;
insert into part1 (id,first,second) select id,first,second from original;
insert into part2 (link,third) select id,third from original;
select count(1) from (select * from original) A;
select count(1) from (select * from part1 inner join part2 on part1.id = part2.link) A;

結果はおおむね同じです。約3倍遅くなります。その、私はあなたのstackoverflowリンクを読みました。それから私は考え始めました:使用したサンプルを見てください。基本的に、MySQLを100万行と100万行を結合する限界までプッシュしました。これは、最悪の等価結合シナリオです。すべてを考慮すると、パフォーマンスはかなり良好です。

疎な配列を構築するためにリンクリストを使用して2次元配列を作成しなければならなかった大学時代を覚えています 。ノードが特定の座標に存在しない場合、配列のデフォルト値はアプリでゼロとして定義されていました。次に、これを想像してみてください。すべての1000000(100万)座標がゼロ以外の値を表す1000x1000スパース配列を作成します。これで、隣接するすべてのノードをマッピングするポインタが少なくとも200万200万個ありました。これは、データの100万の4バイト整数に追加されます。単一の値をフェッチするには、実際のデータを取得するよりもナビゲーションに多くのCPUが必要でした。

Part2から絶対にすべてのキーがある100万行のINNER JOINを実行するには、ナビゲーション(一時テーブルの作成、キーの比較、値の入力)のためにより多くのリソースが必要です。 LEFT JOINの右側があまりスパースでない場合、またはLEFT JOINの左側が大きい場合、非正規化は逆シリアル化されることがあります。あなたの場合、オリジナルをpart1とpart2に分離しても、それらを一緒に頻繁に参照する必要がある場合は何も購入しません。つまり、繰り返しグループを形成しない列を分離することは、真の正規化ではありません。

私が与えた3つのオプションは、part1に最適で、part2を行ごとにフェッチします。

次の場合について考えてください。

  • 必要なのは、part1 の一部の行だけです。
    • mySQL Query Optimizerを信頼していますか
    • クエリをリファクタリングして、part2に結合する前に、part1に断面を取得しますか?
    • 一度に1行だけをpart2から取得しますか?
  • LEFT JOINを使用して、特定のpart1のpart2がないことを学習する
    • アプリでpart2のデフォルト値を設定しますか?
    • そのpart1にpart2を入力しますか?

データの取得をどのように計画するか、単一のクエリで必要なデータの量、およびクエリの構造をより考慮することは、良いパフォーマンスを求める際の決定的な要因になります。

エピローグ

JOINを実行するためのコストのため、パフォーマンスは向上しません。 それは鉛を金に変えようとするようなものです(理論的には可能で、実際的かつ経済的に不可能です)

あなたの最善の策は、テーブルを元の状態のままにすることです。

1
RolandoMySQLDBA