Open hysryt opened 5 years ago
JSONやserialize化したデータを保持する場合も含む。
掲示板のスレッドなど。 親となる行のIDを parent_id で持つ方法がよく使われるが、これでは不十分なケースがある。
ただし、WITH
句が使える場合はその限りではない。
WITH
句は PostgreSQL8.4、MySQL8.0、Oracle11g などで使用可能。
MySQL5.7以下や、SQLite では使用できない。
WITH
句以外の方法として、以下の3つがある。
ルートからの自分までの階層を行ごとに保存する方法。
id | path | comment |
---|---|---|
1 | 1/ | 親 |
2 | 1/2/ | 子 |
3 | 1/3/ | 子2 |
4 | 1/2/4/ | 孫 |
など、カンマ区切りでデータを保持するのと同じデメリットがある。
各行に、nsleft と nsright という2つの数値型カラムを持たせる方法。 id と nsleft, nsright は全くの無関係。 nsleft と nsright は以下のルールによってつける。
id | nsleft | nsright | comment |
---|---|---|---|
1 | 1 | 8 | 親 |
2 | 2 | 5 | 子 |
3 | 6 | 7 | 子2 |
4 | 3 | 4 | 孫 |
親子関係を別テーブルに分けて持つ方法。 閉包テーブルには自分自身と、全ての祖先との親子関係を登録する。
id | comment |
---|---|
1 | 親 |
2 | 子 |
3 | 子2 |
4 | 孫 |
ancestor(先祖) | descendant(子孫) |
---|---|
1 | 1 |
1 | 2 |
2 | 2 |
1 | 3 |
3 | 3 |
1 | 4 |
2 | 4 |
4 | 4 |
他に主キーとして使える列があるのであればそちらを使用する。 ない場合のみ、id列を擬似キーとして追加し、主キーとする。
命名と言う観点から見ても、id より product_id の方が良い
↓こういうの
EAVテーブル(Entity Attribute Value テーブル)と呼ぶ。
WordPressの wp_options
テーブルが主な例。
entity_id | meta_key | meta_value |
---|---|---|
1 | 'color' | '#00000' |
1 | 'font-size' | '16px' |
基本的に1データに対して1列用意した方がいい
EAVテーブルが生まれる原因はいくつかある。
メタデータを全て列にする 使わない場合はnullを入れる 名前はエンタープライズアプリケーションアーキテクチャパターンから
product_id | product_name | color | size | type | start_date | end_date |
---|---|---|---|---|---|---|
1 | '期間限定Tシャツ' | 'red' | 'M' | '期間限定' | 2019-08-01 | 2019-09-01 |
2 | '普通のTシャツ' | 'white' | 'S' | '普通' | null | null |
商品の種別を区別する列が必要になる。 商品の区別が増えるほど列が増えてしまう。
メタデータが必要なテーブルを別テーブルに分ける 名前はエンタープライズアプリケーションアーキテクチャパターンから
product_id | product_name | color | size |
---|---|---|---|
2 | '普通のTシャツ' | 'white' | 'S' |
product_id | product_name | color | size | start_date | end_date |
---|---|---|---|---|---|
1 | '期間限定Tシャツ' | 'red' | 'M' | 2019-08-01 | 2019-09-01 |
全ての商品データを取得する場合は UNION ALL を使う必要がある。 検索が多い場合は不向き。
メタデータだけテーブルを分ける 名前はエンタープライズアプリケーションアーキテクチャパターンから
product_id | product_name | color | size |
---|---|---|---|
1 | '期間限定Tシャツ' | 'red' | 'M' |
2 | '普通のTシャツ' | 'white' | 'S' |
product_id | start_date | end_date |
---|---|---|
1 | 2019-08-01 | 2019-09-01 |
おそらく一番良い方法。
メタデータをJSONなどにまとめて保持する
product_id | product_name | color | size | meta |
---|---|---|---|---|
1 | '期間限定Tシャツ' | 'red' | 'M' | JSONデータ |
2 | '普通のTシャツ' | 'white' | 'S' | からのJSONデータ |
SQLによる検索ができなくなるが、メタデータの属性が頻繁に増減する場合に有効。
ポリモーフィック関連とは、ある列が別の異なる2つのテーブルの主キーを参照すること。
フレームワークなどではポリモーフィック関連が使用されていることがある。 これはフレームワーク内部で参照整合性を保証しているため、ある程度信頼できる。
オブジェクト指向の親クラスのようにする。 親テーブルの主キーを外部キーとして参照すれば参照整合性を保証できる。
そのまま
テーブルを行で分割する。 シャーディング とも呼ばれる。 DBMSの機能によって行うが、SQL標準には定義されていないため、DBMSごとに方法が異なる。
CREATE Bugs (
...
) PARTITION BY HASH ( YEAR(date_reported) )
PARTITIONS 4;
パーティションを指定して検索することで高速化が見込める
SELECT * FROM Bugs PARTITION (HASH ( YEAR(some_date) ))
テーブルを列で分割する。 DBMSに専用の機能はないため、手動で別テーブルを作成する。 サイズの大きいBLOB型やTEXT型の列を別テーブルとすることが多い。
FLOAT型は浮動小数点数であり、誤差が発生する。 誤差が許容できない場合は NUMERIC または DECIMAL 型(固定小数点数)を使用する。
別テーブルを作成し、入力可能な値を行として追加する。 元のテーブルから外部キーとして参照することで、CHECK制約を用いず、入力値を制限できる。
どちらにも一長一短ある
「MENTOR」の原則に基づいてインデックスを作成する
SQLクエリの実行時間を測定する。 実行頻度が高く、実行時間が長いクエリは最適化するメリットが大きい。 実行時間が長いが実行頻度が低い場合はメリットが少ないかもしれない。 クエリパフォーマンスの測定中はキャッシュを無効化にする。
対象のクエリが遅くなっている原因を解析する。 データベースはクエリ実行計画(QEP)と呼ばれる機能でどのインデックスを使用するかを判断している。 QEPの取得方法はDBMSごとに異なる。
インデックスを使用していない部分を探し、インデックスを作成する。
作成したインデックスが有効な効果をもたらしているかテストする。
キャッシュメモリサイズを調整する。
インデックスは長期にわたって使用していると不均衡になってくるため、定期的に再構築を行う。
ANSI SQL 規格にはインデックスに関する記述は存在しない。 ベンダがそれぞれ独自に実装しているため、インデックスを有効に活用するには使用しているDBMSのドキュメントをよく読む必要がある。
どの値が取得されるか曖昧なため。
id | name | season | price |
---|---|---|---|
1 | りんご | 冬 | 100 |
2 | ぶどう | 秋 | 500 |
3 | なし | 秋 | 400 |
4 | すいか | 夏 | 300 |
season でグループ分けし、グループの中で一番 price が高いものの name を取得する
SELECT season, MAX(price), name
FROM fruit
GROUP BY season
これではだめ name は一意に決まらない
WHERE 句でサブクエリを使用して、取得する値を一意にする。
SELECT season, price, name
FROM fruit as f1
WHERE price = (
SELECT MAX(price)
FROM fruit as f2
WHERE f2.season = f1.season
)
サブクエリで同じ season のもののうち、一番高い price を取得している。 外側の SELECT で、取得した price と同じもののみを選択している。
ただし相関サブクエリは行毎に実行されるため、パフォーマンスはあまり良くない。
FROM句でサブクエリを使用して、取得する値を一意にする。
SELECT f1.season, f1.price, f1.name
FROM fruit as f1
INNER JOIN (
SELECT season, MAX(price) AS max_price
FROM fruit
GROUP BY season
) f2
ON f1.season = f2.season
AND f1.price = f2.max_price
導出テーブルはサブクエリは1度実行されるだけなので、相関サブクエリよりもパフォーマンスが良い。 ただし一時テーブルを作成するコストがかかるため、場合によってはさらにパフォーマンスの改善ができる。
SELECT f1.season, f1.price, f1.name
FROM fruit as f1
INNER JOIN fruit as f2
ON f1.season = f2.season
AND f1.price < f2.price
WHERE f2.season IS NULL
パフォーマンスは良いが、SQL文が複雑になりやすい。(保守性が下がる) テーブルの結合数によってはパフォーマンスが下がる可能性もあるので、実際に測定した方が良い。
SELECT season, MAX(price), GROUP_CONCAT(name)
FROM fruit
GROUP BY season
テーブルの中からランダムに1行取得する方法のアンチパターンとして、以下のようなものがある。
SELECT *
FROM product
ORDER BY RAND()
LIMIT 1;
「ランダム値で行をソートし一番上に来たものを返す」というクエリ。 悪いのはランダム値でソートをしている部分。 インデックスのない列でのソートはコストがかかる。 行数が増えれば増えるほど、そのコストは高くなる。
ランダムに関する処理はアプリケーション側で行う方が良い場合が多い。 今回の場合は、
と言う流れで行うのが良い。
1の時点で全てのデータを持って来ればクエリの実行は1回で済むが、メモリを多く使用することになるため好ましくない。
あるいは以下の方法もある。
これは1の時点で行数しか取得していないので先ほどの例よりも効率がよい。 ただしSQLの記述方法がDBMSによって異なる為、DBMSに依存したくない場合は使えない。 また、ランダムで複数件取得したい場合はデータが偏ってしまう。
https://www.amazon.co.jp/dp/4873115892