macOS上でSQL Serverを使用してC#アプリを作成する

# はじめに

この記事では、Microsoft 社が公開している [Build an app using SQL Server][Build-an-app-using-SQLServer] の内容に従い、SQL Server を使用した C# アプリを作成します。

## 環境

- **OS**: macOS Catalina バージョン 10.15.4
- **SQL Server**: SQL Server 2019 
- **.NET Core**: 3.1 LTS


# 環境のセットアップ

ここでは、SQL Server を **Docker** 上で取得します。その後、SQL Server で .NET Core アプリを作成するために必要な依存関係をインストールします。

## SQL Server のインストール

1. macOS で SQL Server を実行するには、SQL Server on Linux の [Docker イメージ][mssql-DockerImage] を使用します。そのためには、[Docker for Mac][DockerDesktop] をインストールする必要があります。
2. Docker 環境に最低 4GB のメモリを設定し、パフォーマンスを評価したい場合は複数のコアを追加することも検討します。これは、メニューバーの [\[環境設定\] -> \[詳細設定\]][DockerPrefAdvance] オプションで行うことができます。
3. 新しい**ターミナルプロンプト**を起動し、以下のコマンドを使用して **SQL Server on Linux** Docker イメージをダウンロードして起動します。`SA_PASSWORD の部分は特殊文字を使用した強力なパスワードを使用するように書き換えてください。`

```shell
sudo docker pull microsoft/mssql-server-linux:2017-latest
docker run -e 'HOMEBREW_NO_ENV_FILTERING=1' -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 -d microsoft/mssql-server-linux
```

筆者は、Docker Desktop をインストールすると一緒に利用可能になる、Docker Compose を利用して、`SQL Server 2019 on Linux` をインストールしています。docker-compose.yaml については、以下の GitHub リポジトリを参照してください。

- [ymasaoka/mssql-docker][GitHub-mssql-docker]

なお、`ウェブ上で公開されている Docker イメージ` の URL は、**Ubuntu ベースの SQL Server 2017 on Linux** の Docker イメージ です。`SQL Server 2019`では、従来の Ubuntu ベースに加え、**RHEL ベース の SQL Server on Linux** の Docker イメージ もサポートされるようになりました。
Ubuntu ベースを利用するか、RHEL ベースを利用するかは、好きな方を選択してください。*(SQL Server における機能に差はありません)*

## Homebrew と .NET Core のインストール

*すでに .NET Core 3.1 LTS がインストールされている場合は、このステップをスキップしてください。*

[公式インストーラー][dotNETCoreInstaller] をダウンロードして、.NET Coreをインストールします。インストーラーは、**Build apps - SDK** のものを選択してください。.NET Core 3.1.x SDK および、.NET Core ランタイムを一緒にインストールできます。

- ASP.NET Core Runtime
- Desktop Runtime
- .NET Core Runtime (上の2つを一緒にしたもの)

.NET Core では、作成したアプリを実行するためのランタイムが、種類によって分けられています。
ダウンロードサイト上では分かれて表示されているため、悩んでしまう可能性がありますが、ここでは気にしないでください。

なお、`本家のサイトにあるリンクは .NET Core 2.0 のダウンロードリンク`になっています。これは**既にサポート切れ、かつ LTS ではない**ため、最新の .NET Core 3.1.x をダウンロードし、インストールするようにしてください。.NET Core 3.1 は LTS バージョンになります。

なお、筆者は、SQL Server 2019 と同様、Docker Compose を使用して、.NET Core 3.1 のコンテナーを作成し、開発を進めています。docker-compose.yaml については、以下の GitHub リポジトリを参照してください。

- [ymasaoka/dotnetcore-docker][GitHub-dotnetcore-docker]

SQL Server および .NET Core 3.1 を Docker 上で利用する場合は、同じ docker-compose.yaml ファイル内に記述します。`container_name` を記述することで、コンテナ名を使ってコンテナ同士の相互通信が可能になります。

```yaml:docker-compose.yaml(例)
version: '3'

services:
  app:
    image: mcr.microsoft.com/dotnet/core/sdk:latest
    container_name: dotnetcoreapp
    tty: true
    ports:
      - 10080:80
    volumes:
      - ./src:/src
    working_dir: "/src"

  mssql:
    image: mcr.microsoft.com/mssql/rhel/server:2019-latest
    container_name: 'mssql2019'
    environment:
      - MSSQL_SA_PASSWORD=databaseadmin@1
      - ACCEPT_EULA=Y
    ports:
      - 1433:1433
    # volumes: # Mounting a volume does not work on Docker for Mac
    #   - ./mssql/log:/var/opt/mssql/log
    #   - ./mssql/data:/var/opt/mssql/data
```


# SQL Server を使った C# アプリケーションを作成

ここでは、以下、2 つのシンプルな C# アプリを作成します。

- 基本的な Insert、Update、Delete、Select を実行するアプリ
- .NET Core の ORM フレームワークの中でも特に人気のある **Entity Framework Core** を利用してInsert、Update、Delete、Select を実行するアプリ

## SQL Server に接続してクエリを実行する C# アプリを作成

開発を行うワークディレクトリに移動し、新しい .NET Core プロジェクトを作成します。
基本的な .NET Core の Program.cs と csproj ファイルを含むプロジェクトディレクトリが作成されます。

```shell
cd ~/
dotnet new console -o SqlServerSample
```

Docker Compose で行う場合は、以下の通りです。

```shell
docker-compose run --rm app dotnet new console -o SqlServerSample
```

**SqlServerSample.csproj** というファイルが SqlServerSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerSample.csproj ファイルを開き、コードを以下の通りに書き換え、System.Data.SqlClient をプロジェクトに追加します。保存してファイルを閉じます。

```xml:SqlServerSample.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
  </ItemGroup>

</Project>
```

SqlServerSample ディレクトリ以下にある Program.cs ファイルを開き、コードを以下の通りに書き換え、保存してファイルを閉じます。
`ユーザー名とパスワードを自分のものに置き換えることを忘れないでください。`

```csharp:Program.cs
using System;
using System.Data.SqlClient;

namespace SqlServerSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                // 接続文字列の構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";   // 接続先の SQL Server インスタンス
                builder.UserID = "sa";              // 接続ユーザー名
                builder.Password = "your_password"; // 接続パスワード
                builder.InitialCatalog = "master";  // 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;  // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                // SQL Server に接続
                Console.Write("SQL Server に接続しています... ");
                using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
                {
                    connection.Open();
                    Console.WriteLine("接続成功。");
                }
            }
            catch (SqlException e)
            {
                Console.WriteLine(e.ToString());
            }

            Console.WriteLine("すべてが完了しました。任意のキーを押してアプリを終了します...");
            Console.ReadKey(true);
        }
    }
}
```

SqlServerSample ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。

```shell
cd ~/SqlServerSample
dotnet restore
```

完了したら、ビルド実行を行います。

```shell
dotnet run
```

*なお、Docker Compose で実行している場合は、以下のようなコマンドを実行することで上記を実現できます。*

```shell
docker-compose run -w /src/SqlServerSample --rm app dotnet restore
docker-compose run -w /src/SqlServerSample --rm app dotnet run
```

![SqlServerSample1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348116/fcd8f1e1-7d7a-a8c2-a832-feaf46940e3f.gif)

これで、SQL Server に接続を行うコンソールアプリができました。ただし、このアプリでは単にデータベースへの接続だけを行っているだけで、クエリは実行していません。
次に、Program.cs 内にコードを追加して、データベースやテーブルの作成、INSERT/UPDATE/DELETE/SELECT などのクエリを実行するように変更します。
`ユーザー名とパスワードは自分のものに置き換えることを忘れないでください。`
書き換えた後、ファイルを保存し、プロジェクトをビルドして実行します。

```csharp:Program.cs
using System;
using System.Text;
using System.Data.SqlClient;

namespace SqlServerSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("SQL Server に接続し、Create、Read、Update、Delete 操作のデモを行います。");

                // 接続文字列の構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";   // 接続先の SQL Server インスタンス
                builder.UserID = "sa";              // 接続ユーザー名
                builder.Password = "your_password"; // 接続パスワード
                builder.InitialCatalog = "master";  // 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;  // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                // SQL Server に接続
                Console.Write("SQL Server に接続しています... ");
                using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
                {
                    connection.Open();
                    Console.WriteLine("接続成功。");

                    // サンプルデータベースの作成
                    Console.Write("既に作成されている SampleDB データベースを削除し、再作成します... ");
                    String sql = "DROP DATABASE IF EXISTS [SampleDB]; CREATE DATABASE [SampleDB]";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("SampleDB データベースを作成しました。");
                    }

                    // テーブルを作成しサンプルデータを登録
                    Console.Write("サンプルテーブルを作成しデータを登録します。任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    StringBuilder sb = new StringBuilder();
                    sb.Append("USE SampleDB; ");
                    sb.Append("CREATE TABLE Employees ( ");
                    sb.Append(" Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, ");
                    sb.Append(" Name NVARCHAR(50), ");
                    sb.Append(" Location NVARCHAR(50) ");
                    sb.Append("); ");
                    sb.Append("INSERT INTO Employees (Name, Location) VALUES ");
                    sb.Append("(N'Jared', N'Australia'), ");
                    sb.Append("(N'Nikita', N'India'), ");
                    sb.Append("(N'Tom', N'Germany'); ");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("作成完了");
                    }

                    // INSERT デモ
                    Console.Write("テーブルに新しい行を挿入するには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sb.Clear();
                    sb.Append("INSERT Employees (Name, Location) ");
                    sb.Append("VALUES (@name, @location);");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.Parameters.AddWithValue("@name", "Jake");
                        command.Parameters.AddWithValue("@location", "United States");
                        int rowsAffected = command.ExecuteNonQuery();
                        Console.WriteLine(rowsAffected + " 行 挿入されました");
                    }

                    // UPDATE デモ
                    String userToUpdate = "Nikita";
                    Console.Write("ユーザー '" + userToUpdate + "' の 'Location' を更新するには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sb.Clear();
                    sb.Append("UPDATE Employees SET Location = N'United States' WHERE Name = @name");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.Parameters.AddWithValue("@name", userToUpdate);
                        int rowsAffected = command.ExecuteNonQuery();
                        Console.WriteLine(rowsAffected + " 行 更新されました");
                    }

                    // DELETE デモ
                    String userToDelete = "Jared";
                    Console.Write("ユーザー '" + userToDelete + "' を削除するには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sb.Clear();
                    sb.Append("DELETE FROM Employees WHERE Name = @name;");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.Parameters.AddWithValue("@name", userToDelete);
                        int rowsAffected = command.ExecuteNonQuery();
                        Console.WriteLine(rowsAffected + " 行 削除されました");
                    }

                    // READ デモ
                    Console.WriteLine("テーブルからデータを読み取るには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sql = "SELECT Id, Name, Location FROM Employees;";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        using (SqlDataReader reader = command.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                Console.WriteLine("{0} {1} {2}", reader.GetInt32(0), reader.GetString(1), reader.GetString(2));
                            }
                        }
                    }
                }
            }
            catch (SqlException e)
            {
                Console.WriteLine(e.ToString());
            }

            Console.WriteLine("すべて完了しました。任意のキーを押して終了します...");
            Console.ReadKey(true);
        }
    }
}
```

![SqlServerSample2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348116/10a55446-6224-8896-c9ee-94abc5f0236d.gif)

これで、macOS 上の .NET Core を使って、初めて C# + SQL Server アプリを作成できました。次は、ORM を使って C# アプリを作成します。


## .NET Core で Entity Framework Core ORM を使用して SQL Server に接続する C# アプリを作成

ワークディレクトリに戻り、新しい.NET Coreプロジェクトを作成します。

```shell
cd ~/
dotnet new console -o SqlServerEFSample
```

Docker Compose で行う場合は、以下の通りです。

```shell
docker-compose run --rm app dotnet new console -o SqlServerSampleEF
```

SqlServerEFSample.csproj というファイルが SqlServerEFSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerEFSample.csproj ファイルを開き、コードを以下の通りに書き換え、Entity Framework Core をプロジェクトに追加します。保存してファイルを閉じます。

```xml:SqlServerEFSample.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.4" />
  </ItemGroup>

</Project>
```

このサンプルでは、2つのテーブルを作成します。1つ目は「ユーザー」に関するデータを保持し、もう1つは「タスク」に関するデータを保持するものです。

**User.cs を作成します。**

User クラスを定義します。SqlServerEFSample ディレクトリ以下に User.cs ファイルを作成します。
このクラスは、User テーブルに紐づくモデルのクラスです。書き換えた後、ファイルを保存して閉じます。
`この時点では、Task クラスがないためコンパイルエラーとなりますが、問題ありません。`

```csharp:User.cs
using System;
using System.Collections.Generic;

namespace SqlServerEFSample
{
    public class User
    {
        public int UserId { get; set; }
        public String FirstName { get; set; }
        public String LastName { get; set; }
        public virtual IList<Task> Tasks { get; set; }

        public String GetFullName()
        {
            return this.FirstName + " " + this.LastName;
        }
        public override string ToString()
        {
            return "User [id=" + this.UserId + ", name=" + this.GetFullName() + "]";
        }
    }
}
```

**Task.cs を作成します。**

Task クラスを定義します。SqlServerEFSample ディレクトリ以下に Task.cs ファイルを作成します。
このクラスは、Task テーブルに紐づくモデルのクラスです。書き換えた後、ファイルを保存して閉じます。

```csharp:Task.cs
using System;

namespace SqlServerEFSample
{
    public class Task
    {
        public int TaskId { get; set; }
        public string Title { get; set; }
        public DateTime DueDate { get; set; }
        public bool IsComplete { get; set; }
        public virtual User AssignedTo { get; set; }

        public override string ToString()
        {
            return "Task [id=" + this.TaskId + ", title=" + this.Title + ", dueDate=" + this.DueDate.ToString() + ", IsComplete=" + this.IsComplete + "]";
        }
    }
}
```

**EFSampleContext.cs を作成します。**

EFSampleContext クラスを定義します。SqlServerEFSample ディレクトリ以下に EFSampleContext.cs ファイルを作成します。
このクラスは、Entity Framework Core を使用し、.NET オブジェクトを利用してデータのクエリ、挿入、更新、および削除を行うためのクラスです。User クラスと Task クラスを使用しています。
書き換えた後、ファイルを保存して閉じます。

```csharp:EFSampleContext.cs
using Microsoft.EntityFrameworkCore;

namespace SqlServerEFSample
{
    public class EFSampleContext : DbContext
    {
        string _connectionString;
        public EFSampleContext(string connectionString)
        {
            this._connectionString = connectionString;
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(this._connectionString);
        }

        public DbSet<User> Users { get; set; }
        public DbSet<Task> Tasks { get; set; }
    }
}
```

*Entity Framework (.NET Framework) と違う点としては、OnConfiguring メソッドが新たにオーバーライドされ、逆に Database.SetInitializer<TContext>(IDatabaseInitializer<TContext>) を EFSampleContext のコンストラクタ内で指定しなくなっている点です。*

最後に **Program.cs** を更新します。これまで作成したクラスを使用するための設定を行います。
`ユーザー名とパスワードを自分のものに更新することを忘れないでください。`
保存してファイルを閉じます。

```csharp:Program.cs
using System;
using System.Linq;
using System.Data.SqlClient;
using System.Collections.Generic;

namespace SqlServerEFSample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("** Entity Framework Core と SQL Server を使用した C# CRUD のサンプル **\n");
            try
            {
                // 接続文字列を構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";     // 接続先の SQL Server インスタンス
                builder.UserID = "sa";                // 接続ユーザー名
                builder.Password = "your_password";   // 接続パスワード
                builder.InitialCatalog = "EFSampleDB";// 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;    // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                using (EFSampleContext context = new EFSampleContext(builder.ConnectionString))
                {
                    context.Database.EnsureDeleted();
                    context.Database.EnsureCreated();
                    Console.WriteLine("C#のクラスからデータベーススキーマを作成しました。");

                    // Create デモ: ユーザーインスタンスを作成し、データベースに保存
                    User newUser = new User { FirstName = "Anna", LastName = "Shrestinian" };
                    context.Users.Add(newUser);
                    context.SaveChanges();
                    Console.WriteLine("\n作成されたユーザー: " + newUser.ToString());

                    // Create デモ: タスクインスタンスを作成し、データベースに保存
                    Task newTask = new Task() { Title = "Ship Helsinki", IsComplete = false, DueDate = DateTime.Parse("04-01-2017") };
                    context.Tasks.Add(newTask);
                    context.SaveChanges();
                    Console.WriteLine("\nCreated Task: " + newTask.ToString());

                    // Association demo: Assign task to user
                    newTask.AssignedTo = newUser;
                    context.SaveChanges();
                    Console.WriteLine("\n作成されたタスク: '" + newTask.Title + "' 割り当てられたユーザー: '" + newUser.GetFullName() + "'");

                    // Read デモ: ユーザー 'Anna' に割り当てられた未完了のタスクを見つける
                    Console.WriteLine("\n'Anna' に割り当てられた未完了のタスク:");
                    var query = from t in context.Tasks
                                where t.IsComplete == false &&
                                t.AssignedTo.FirstName.Equals("Anna")
                                select t;
                    foreach(var t in query)
                    {
                        Console.WriteLine(t.ToString());
                    }

                    // Update デモ: タスクの '期限' を変更
                    Task taskToUpdate = context.Tasks.First(); // 最初のタスクを取得
                    Console.WriteLine("\nタスクをアップデート中: " + taskToUpdate.ToString());
                    taskToUpdate.DueDate = DateTime.Parse("06-30-2016");
                    context.SaveChanges();
                    Console.WriteLine("変更された期限: : " + taskToUpdate.ToString());

                    // Delete デモ: 2016年が期限になっているすべてのタスクを削除
                    Console.WriteLine("\n期限が2016年になっているすべてのタスクを削除します。");
                    DateTime dueDate2016 = DateTime.Parse("12-31-2016");
                    query = from t in context.Tasks
                            where t.DueDate < dueDate2016
                            select t;
                    foreach(Task t in query)
                    {
                        Console.WriteLine("Deleting task: " + t.ToString());
                        context.Tasks.Remove(t);
                    }
                    context.SaveChanges();

                    // 'Delete' 操作の後にタスクを表示 - 0個のタスクがあるはず
                    Console.WriteLine("\n削除後のタスク:");
                    List<Task> tasksAfterDelete = (from t in context.Tasks select t).ToList<Task>();
                    if (tasksAfterDelete.Count == 0)
                    {
                        Console.WriteLine("[なし]");
                    }
                    else
                    {
                        foreach (Task t in query)
                        {
                            Console.WriteLine(t.ToString());
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }

            Console.WriteLine("すべて完了しました。任意のキーを押して終了します...");
            Console.ReadKey(true);
        }
    }
}
```

*EFSampleContext クラスのコンストラクタで Database.SetInitializer<TContext>(IDatabaseInitializer<TContext>) を行っていないため、Program.cs 内で context.Database.EnsureDeleted() と context.Database.EnsureCreated() が行われていますね。*

SqlServerSampleEF ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。

```shell
cd ~/SqlServerEFSample
dotnet restore
```

完了したら、ビルド実行を行います。

```shell
dotnet run
```

*Docker Compose で実行する場合は、以下のコマンドを実行してください。*

```shell
docker-compose run -w /src/SqlServerEFSample --rm app dotnet restore
docker-compose run -w /src/SqlServerEFSample --rm app dotnet run
```

![SqlServerEFSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348116/cbfaf97e-18aa-0562-6aef-481908c3e970.gif)

これで、2つ目の C# アプリの作成が終わりました。最後に、SQL Server の カラムストア機能を使って C# アプリを高速化する方法について学びます。


# C# アプリを 100 倍速にする

これまでで基本的なことは理解できたと思います。最後は、SQL Server を使用してアプリをより良くする方法を見てみます。このモジュールでは、カラムストアインデックスの簡単な例と、カラムストアインデックスがどのようにデータ処理速度を向上させるかを確認します。カラムストアインデックスは、従来の列ストアインデックスに比べて、分析ワークロードでは最大 100 倍のパフォーマンス向上、データ圧縮では最大 10 倍のパフォーマンス向上を実現できます。

カラムストアインデックスの機能を確認するために、500 万行のサンプルデータベースとサンプルテーブルを作成し、カラムストアインデックスを追加する前と後の簡単なクエリを実行する C# アプリケーションを作成します。

ワークディレクトリに戻り、新しい.NET Coreプロジェクトを作成します。

```shell
cd ~/
dotnet new console -o SqlServerColumnstoreSample
```

Docker Compose で行う場合は、以下の通りです。

```shell
docker-compose run --rm app dotnet new console -o SqlServerColumnstoreSample
```

SqlServerColumnstoreSample.csproj というファイルが SqlServerColumnstoreSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerColumnstoreSample.csproj ファイルを開き、コードを以下の通りに書き換え、System.Data.SqlClient をプロジェクトに追加します。保存してファイルを閉じます。

```xml:SqlServerColumnstoreSample.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
  </ItemGroup>

</Project>
```

Program.cs の内容を書き換えます。
`ユーザー名とパスワードは自分のものに置き換えることを忘れないでください。`
保存してファイルを閉じます。

```csharp:Program.cs
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SqlServerColumnstoreSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("*** SQL Server カラムストアのデモ ***");

                // 接続文字列の構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";   // 接続先の SQL Server インスタンス
                builder.UserID = "sa";              // 接続ユーザー名
                builder.Password = "your_password"; // 接続パスワード
                builder.InitialCatalog = "master";  // 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;  // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                // SQL Server に接続
                Console.Write("SQL Serverへ接続しています... ");
                using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
                {
                    connection.Open();
                    Console.WriteLine("接続完了。");

                    // サンプルデータベースの作成
                    Console.Write("'SampleDB' を再作成しています... ");
                    String sql = "DROP DATABASE IF EXISTS [SampleDB]; CREATE DATABASE [SampleDB]";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("完了。");
                    }

                    // 'Table_with_5M_rows' テーブルに500万行を挿入
                    Console.Write("テーブル 'Table_with_5M_rows' に500万行を挿入します。1分ほどかかりますが、お待ちください... ");
                    StringBuilder sb = new StringBuilder();
                    sb.Append("USE SampleDB; ");
                    sb.Append("WITH a AS (SELECT * FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS a(a))");
                    sb.Append("SELECT TOP(5000000)");
                    sb.Append("ROW_NUMBER() OVER (ORDER BY a.a) AS OrderItemId ");
                    sb.Append(",a.a + b.a + c.a + d.a + e.a + f.a + g.a + h.a AS OrderId ");
                    sb.Append(",a.a * 10 AS Price ");
                    sb.Append(",CONCAT(a.a, N' ', b.a, N' ', c.a, N' ', d.a, N' ', e.a, N' ', f.a, N' ', g.a, N' ', h.a) AS ProductName ");
                    sb.Append("INTO Table_with_5M_rows ");
                    sb.Append("FROM a, a AS b, a AS c, a AS d, a AS e, a AS f, a AS g, a AS h;");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("完了。");
                    }

                    // カラムストアインデックスなしで SQL クエリを実行
                    double elapsedTimeWithoutIndex = SumPrice(connection);
                    Console.WriteLine("カラムストアインデックスなしのクエリ時間: " + elapsedTimeWithoutIndex + "ms");

                    // カラムストアインデックスを追加
                    Console.Write("'Table_with_5M_rows' テーブルにカラムストアインデックスを追加中... ");
                    sql = "CREATE CLUSTERED COLUMNSTORE INDEX columnstoreindex ON Table_with_5M_rows;";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("完了。");
                    }

                    // カラムストアインデックスが追加された後、再度同じ SQL クエリを実行
                    double elapsedTimeWithIndex = SumPrice(connection);
                    Console.WriteLine("カラムストアありのクエリ時間: " + elapsedTimeWithIndex + "ms");

                    // カラムストアインデックスの追加によるパフォーマンス向上を計算
                    Console.WriteLine("カラムストアインデックスによる性能向上: "
                        + Math.Round(elapsedTimeWithoutIndex / elapsedTimeWithIndex) + "x!");
                }
                Console.WriteLine("すべて完了しました。任意のキーを押して終了します...");
                Console.ReadKey(true);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        public static double SumPrice(SqlConnection connection)
        {
            String sql = "SELECT SUM(Price) FROM Table_with_5M_rows";
            long startTicks = DateTime.Now.Ticks;
            using (SqlCommand command = new SqlCommand(sql, connection))
            {
                try
                {
                    command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                    var sum = command.ExecuteScalar();
                    TimeSpan elapsed = TimeSpan.FromTicks(DateTime.Now.Ticks) - TimeSpan.FromTicks(startTicks);
                    return elapsed.TotalMilliseconds;
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }
            }
            return 0;
        }
    }
}
```

SqlServerColumnstoreSample ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。

```shell
cd ~/SqlServerColumnstoreSample
dotnet restore
```

完了したら、ビルド実行を行います。

```shell
dotnet run
```

*Docker Compose で実行する場合は、以下のコマンドを実行してください。*

```shell
docker-compose run -w /src/SqlServerColumnstoreSample --rm app dotnet restore
docker-compose run -w /src/SqlServerColumnstoreSample --rm app dotnet run
```

![SqlServerColumnStoreSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348116/0665ddfc-06bd-1450-c23e-1808d414f1c0.gif)

おめでとうございます。カラムストアインデックスを使って C# アプリを高速化しました！


# おわりに

以上で、「macOS上でSQL Serverを使用してC#アプリを作成する」は終了です。[Build an app using SQL Server][Build-an-app-using-SQLServer] には、他言語での SQL Server アプリを作成するチュートリアルがあります。ぜひ、他の言語でも試してみてください。


---

[Build-an-app-using-SQLServer]:https://sqlchoice.azurewebsites.net/en-us/sql-server/developer-get-started
[mssql-DockerImage]:https://hub.docker.com/r/microsoft/mssql-server-linux
[DockerDesktop]:https://docs.docker.com/engine/installation/mac
[DockerPrefAdvance]:https://docs.docker.com/docker-for-mac/#advanced
[dotNETCoreInstaller]:https://dotnet.microsoft.com/download/dotnet-core/3.1

[GitHub-mssql-docker]:https://github.com/ymasaoka/docker-mssql
[GitHub-dotnetcore-docker]:https://github.com/ymasaoka/dotnetcore-docker