SQL Server のデータパーティションについて

# データパーティションとは
データパーティションとは、データの格納場所を分割することができる機能です。
1つのテーブルを日付の範囲などで、複数に行分割することができます。

## パーティション化することのメリットとは
行を分割することによって、アクセスを迅速かつ効率的に行うことができます。
例えば、過去10年間分の膨大なデータを保持しているテーブルがあったとします。
実際によくアクセスするのは直近2年くらい、といった場合に、年で分割しておくと、アクセスを必要なデータのみにあらかじめ絞る事が出来るので、処理を効率化することができます。
実態は分割されていても理論的には1つのテーブルに見えているので、クエリを書く側はパーティション化されている事を意識することなく使用することができます。

パーティション化していない場合
![パーティション化していない場合](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/5fd95fa3-1230-273e-10c6-35f6fb326753.jpeg)

パーティション化していれば、効率よくデータにアクセスできる
![パーティション化している場合](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/138f4d54-37db-e2dd-8a59-d5e432771859.jpeg)

# 実際にパーティションを設定してみる
パーティション化するためには、事前に以下の設定が必要です。

1. ファイルグループの作成
1. ファイルの作成
1. パーティション関数の作成
1. パーティション構成の作成

今回は、以下のような売上のデータを、OrderDateの日付で年ごとに分割したいと思います。
![サンプルデータ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/d0bc0c1f-cf1e-cc15-8185-9c6b6e099780.jpeg)

SQL Server Management Studio で操作していきます。

## ファイルグループの作成
まずはファイルグループを作成します。
[partitionTest]というDBに設定をしていきます。
今回は日付で分割し、2017年以前、2018年、2019年、2020年、2021年以降という5つのファイルグループを作成します。

```sql
--2017年以前
ALTER DATABASE [partitionTest]
ADD FILEGROUP [FG2017izen]

--2018年
ALTER DATABASE [partitionTest]
ADD FILEGROUP [FG2018]

--2019年
ALTER DATABASE [partitionTest]
ADD FILEGROUP [FG2019]

--2020年
ALTER DATABASE [partitionTest]
ADD FILEGROUP [FG2020]

--2021年以降
ALTER DATABASE [partitionTest]
ADD FILEGROUP [FG2021ikou]
```

### ファイルグループが作成されたか確認する方法
オブジェクトエクスプローラーのDB名の上で右クリック→「プロパティ」を開きます。
![プロパティ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/75651330-9154-abf7-15e2-2f953c2f0786.jpeg)

「ファイルグループ」を選択すると、先ほど作成したファイルグループが表示されます。
![ファイルグループ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/2bc7710b-0dd1-2c88-bc17-051ff0a4b1c6.jpeg)

## ファイルの作成
次に、ファイルを作成します。
サイズやパスなどは適宜変更してください。

```sql
--2017年以前
ALTER DATABASE [partitionTest]
ADD FILE (
	 NAME = N'partitionTest_FG2017izen'
	,FILENAME = N'パス\partitionTest_FG2017izen.ndf'
	,SIZE = 5120KB
	,MAXSIZE = UNLIMITED
	,FILEGROWTH = 1024KB
)
TO FILEGROUP [FG2017izen]   --先ほど作成したファイルグループを指定する

--2018年
ALTER DATABASE [partitionTest]
ADD FILE (
	 NAME = N'partitionTest_FG2018'
	,FILENAME = N'パス\partitionTest_FG2018.ndf'
	,SIZE = 5120KB
	,MAXSIZE = UNLIMITED
	,FILEGROWTH = 1024KB
)
TO FILEGROUP [FG2018]   --先ほど作成したファイルグループを指定する

--2019年
ALTER DATABASE [partitionTest]
ADD FILE (
	 NAME = N'partitionTest_FG2019'
	,FILENAME = N'パス\partitionTest_FG2019.ndf'
	,SIZE = 5120KB
	,MAXSIZE = UNLIMITED
	,FILEGROWTH = 1024KB
)
TO FILEGROUP [FG2019]   --先ほど作成したファイルグループを指定する

--2020年
ALTER DATABASE [partitionTest]
ADD FILE (
	 NAME = N'partitionTest_FG2020'
	,FILENAME = N'パス\partitionTest_FG2020.ndf'
	,SIZE = 5120KB
	,MAXSIZE = UNLIMITED
	,FILEGROWTH = 1024KB
)
TO FILEGROUP [FG2020]   --先ほど作成したファイルグループを指定する

--2021年以降
ALTER DATABASE [partitionTest]
ADD FILE (
	 NAME = N'partitionTest_FG2021ikou'
	,FILENAME = N'パス\partitionTest_FG2021ikou.ndf'
	,SIZE = 5120KB
	,MAXSIZE = UNLIMITED
	,FILEGROWTH = 1024KB
)
TO FILEGROUP [FG2021ikou]   --先ほど作成したファイルグループを指定する
```

### ファイルが作成されたか確認する方法
先ほどと同様、「プロパティ」から「File」を選択すると、先ほど作成したファイルが表示されます。
![ファイル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/c1f43f15-5594-e992-b42e-04cf8d78f65f.jpeg)

上記で指定したフォルダをみると、ちゃんとファイルが作成されています。
![フォルダ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/85fcb4ab-2384-10ad-7c1a-a272ed36b9fd.jpeg)

## パーティション関数の作成
次はデータをファイルグループに振り分けるための関数を作成します。
今回は日付で分割する「funcPartitionDate」という関数を作成して、2017年以前、2018年、2019年、2020年、2021年以降と分けていきます。

```sql
CREATE PARTITION FUNCTION funcPartitionDate(datetime) AS RANGE RIGHT FOR VALUES (
	 '2018/01/01'
	,'2019/01/01'
	,'2020/01/01'
	,'2021/01/01'
)
```

RANGE句の指定により、分割する値の範囲が異なります。

### RANGE RIGHTを指定した場合
| パーティション | 値（RANGE RIGHTを指定した場合） |
| --- | --- |
| 1 | column < 2018/01/01 |
| 2 | column >= 2018/01/01 AND column < 2019/01/01 |
| 3 | column >= 2019/01/01 AND column < 2020/01/01 |
| 4 | column >= 2020/01/01 AND column < 2021/01/01 |
| 5 | column >= 2021/01/01 |

### RANGE LEFTを指定した場合
| パーティション | 値（RANGE LEFTを指定した場合） |
| --- | --- |
| 1 | column <= 2018/01/01 |
| 2 | column > 2018/01/01 AND column <= 2019/01/01 |
| 3 | column > 2019/01/01 AND column <= 2020/01/01 |
| 4 | column > 2020/01/01 AND column <= 2021/01/01 |
| 5 | column > 2021/01/01 |

### パーティション関数が作成されたか確認する方法
オブジェクトエクスプローラーを最新の状態に更新します。
DB名→「ストレージ」→「パーティション関数」を選択すると、先ほど作成した関数が表示されます。
![パーティション関数](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/9896e964-7c6e-be1b-6e68-6a456e5aa4b2.jpeg)

## パーティション構成の作成
パーティション構成を作成します。
パーティション関数で指定されたパーティションに対し、ファイルグループを割り当てます。
複数のパーティションを同じファイルグループを指定することも可能です。

```sql
CREATE PARTITION SCHEME scmPartitionDate AS PARTITION funcPartitionDate TO (
	 FG2017izen		--        ～20171231
	,FG2018			--20180101～20181231
	,FG2019			--20190101～20191231
	,FG2020			--20200101～20201231
	,FG2021ikou		--20210101～        
)
```

### パーティション構成が作成されたか確認する方法
先程と同様、オブジェクトエクスプローラーを最新の状態に更新します。
DB名→「ストレージ」→「パーティション構成」を選択すると、先ほど作成した構成が表示されます。
![パーティション構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/c197fc1f-9c2b-e3ed-d50a-71da5c0bbda8.jpeg)


## テーブルの作成
パーティションの準備が出来たので、実際にテーブルを作成します。
テーブルを作成する時は、利用するパーティション構成と、分割したい列を指定します。

```sql
CREATE TABLE [Sales](
	 [SalesOrderID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL
	,[RevisionNumber] [tinyint] NOT NULL
	,[OrderDate] [datetime] NOT NULL
	,[Status] [tinyint] NOT NULL
	,[CustomerID] [int] NOT NULL
	,[SubTotal] [money] NOT NULL
	,[ModifiedDate] [datetime] NOT NULL
) ON [scmPartitionDate]([OrderDate])	--パーティション構成と分割列を指定する

```
これでパーティションの構築が完了しました。
このテーブルにデータを登録すると、パーティション関数で指定した値の範囲にしたがって、それぞれのファイルグループへデータが保存されます。
今回はテストデータを約10万件ほど登録しました。

# 実際に実行してパフォーマンスを調べてみる
パーティション化したテーブルとしていないテーブルとで比較してみます。
テーブルの内容は、パーティションありもなしも同じです。

パーティションありのテーブル：[Sales]
パーティションなしのテーブル：[Sales_NP]

テーブルの年ごとのレコード数はこんなかんじです。
![年ごとの件数](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/1122e70c-8fa6-7e51-7c8b-1b0b0a87a2fe.jpeg)

## 実行プランを表示させる
パフォーマンスを調べるために、実行プランの設定を行います。
「クエリ」→「実際の実行プランを含める」をクリックすると、クエリ実行時に使用される実際のクエリ実行プランが表示されます。
![実際の実行プランを含める](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/077c2b3e-7ccd-44fb-8b83-349aa7ded00f.jpeg)

## 全レコードをSELECT
まず、全レコードを表示してみます。

```sql
SELECT * FROM [Sales]
```

「実行プラン」タブを開いて、「Table Scan」をクリックします。
![実行プランタブ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/3ed2d0a8-3019-d177-222d-f2a9b36bd794.jpeg)

「Table Scan」をクリックした状態で、「プロパティウィンドウ」の「実際にアクセスされたパーティション」を確認します。
![全レコードをSELECT](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/824256df-e20b-32df-7335-e949acba5436.jpeg)

全レコードを表示させているので、1～5のすべてのパーティションにアクセスしています。
[Sales_NP]テーブルはパーティションの設定をしていないので、「実際にアクセスされたパーティション」の項目自体がありません。

## 日付を指定してSELECT
次に、条件で日付を指定して表示してみます。

```sql
SELECT * FROM [Sales]
WHERE [OrderDate] >= '2019/01/01' And [OrderDate] <= '2019/12/31'
```

![日付を指定してSELECT](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/e6f33edf-1c58-2dd2-5232-94853b0caeb6.jpeg)

条件を2019年に絞ったので、アクセスしたパーティションが「3」のみになっています。

パーティションありとなしで同じ条件で実行した際の実行プランを比較すると、
パーティションありの方が若干ですがコストが低くなっています。
今回はテストデータの件数がそこまで多くないのであまり差が出ていませんが、大規模なテーブルであればより差が出るとおもいます。

パーティションあり
![パーティションあり](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/f5555075-f0ac-8fd2-258c-3c0958bba41f.jpeg)

パーティションなし
![パーティションなし](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/ceead190-c782-d07b-ed5f-21458f39789b.jpeg)

比較

| 項目名 | パーティションあり | パーティションなし |
| --- | ---: | ---: |
| クエリコスト（バッチ相対） | 24% | 76% |
| CUPの推定コスト | 0.0364397 | 0.114913 |
| I/Oの推定コスト | 0.132979 | 0.414315 |
| 実際の経過CUP時間（ミリ秒） | 23 | 40 |

## パーティションの恩恵を受けられないクエリ
折角パーティションの設定をしても、クエリの書き方によってはうまくパフォーマンスが上がらない場合があります。
例を紹介します。

```sql
--LIKE文を使用している
SELECT * FROM [Sales]
WHERE [OrderDate] like '%2019%'

--分割列に対して関数を使用している
SELECT * FROM [Sales]
WHERE format([OrderDate],'yyyy') = '2019'
```

上記のパターンは2019年で絞っているつもりでも、1～5のすべてのパーティションにアクセスしてしまいます。
![パーティションの恩恵を受けられないクエリ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/2b18a727-964c-6ded-ca6a-e0dfc3c3cc9c.jpeg)

パフォーマンス向上のためには、クエリの書き方にも注意が必要です。

## どのデータがどのパーティションに属しているか確認する
$PARTITION (Transact-SQL) 関数を使用すると、~~行が属しているパーティション番号を得ることができます。~~ 指定したパーティション関数に対して、パーティション分割列の値のセットがマップされるパーティション番号を取得できます。
※20210309追記 実際に格納されているパーティションの番号を返すわけではなく、関数でセットされるであろうパーティション番号を返してくれるようです。

```sql
--パーティション番号を表示する
SELECT
	$partition.[funcPartitionDate]([OrderDate]) AS [パーティション番号]
	,*
FROM 
	[Sales]
```

実行結果
![パーティション番号を表示する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230850/be6336de-eddc-9b03-a2e4-2a1a6234885f.jpeg)

# おわりに
インデックスと同様、パーティションも奥が深いです。
設定だけしてやった気分にならずに、実行プランを確認しながらパフォーマンス向上に取り組みたいとおもいます。

参考になったサイト
[データパーティショニングで巨大DBも楽々管理 (1/3)：SQL Server 2005を使いこなそう（11） - ＠IT](https://www.atmarkit.co.jp/ait/spv/0612/21/news118.html)

[パーティション テーブルとパーティション インデックス - SQL Server | Microsoft Docs](https://docs.microsoft.com/ja-jp/sql/relational-databases/partitions/partitioned-tables-and-indexes?view=sql-server-ver15)

[実際の実行プランの表示 - SQL Server | Microsoft Docs](https://docs.microsoft.com/ja-jp/sql/relational-databases/performance/display-an-actual-execution-plan?view=sql-server-ver15)

[$PARTITION (Transact-SQL) - SQL Server | Microsoft Docs](https://docs.microsoft.com/ja-jp/sql/t-sql/functions/partition-transact-sql?redirectedfrom=MSDN&view=sql-server-ver15)