先週、AlwaysEncryptedの制限とパフォーマンスへの影響について書きました。主に次の変更により、さらにテストを実行した後、フォローアップを投稿したいと思いました。
- ローカルのテストを追加して、ネットワークオーバーヘッドが重要であるかどうかを確認しました(以前は、テストはリモートのみでした)。ただし、これらは同じ物理ホスト上の2つのVMであり、実際には真のベアメタル分析ではないため、「ネットワークオーバーヘッド」を引用符で囲む必要があります。
- テーブルをより現実的にするために(暗号化されていない)列をいくつか追加しました(ただし、実際にはそれほど現実的ではありません)。
DateCreated DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), IsActive BIT NOT NULL DEFAULT 1
次に、それに応じて取得手順を変更しました:
ALTER PROCEDURE dbo.RetrievePeople AS BEGIN SET NOCOUNT ON; SELECT TOP (100) LastName, Salary, DateCreated, DateModified, Active FROM dbo.Employees ORDER BY NEWID(); END GO - テーブルを切り捨てる手順を追加しました(以前はテスト間で手動で行っていました)。
CREATE PROCEDURE dbo.Cleanup AS BEGIN SET NOCOUNT ON; TRUNCATE TABLE dbo.Employees; END GO
- タイミングを記録する手順を追加しました(以前はコンソール出力を手動で解析していました):
USE Utility; GO CREATE TABLE dbo.Timings ( Test NVARCHAR(32), InsertTime INT, SelectTime INT, TestCompleted DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), HostName SYSNAME NOT NULL DEFAULT HOST_NAME() ); GO CREATE PROCEDURE dbo.AddTiming @Test VARCHAR(32), @InsertTime INT, @SelectTime INT AS BEGIN SET NOCOUNT ON; INSERT dbo.Timings(Test,InsertTime,SelectTime) SELECT @Test,@InsertTime,@SelectTime; END GO - ページ圧縮を使用するデータベースのペアを追加しました。暗号化された値は十分に圧縮されないことは誰もが知っていますが、これは暗号化された列を持つテーブルでも一方的に使用できる極性化機能であるため、これらもプロファイルします。 (さらに2つの接続文字列を
App.Configに追加しました 。)<connectionStrings> <add name="Normal" connectionString="...;Initial Catalog=Normal;"/> <add name="Encrypt" connectionString="...;Initial Catalog=Encrypt;Column Encryption Setting=Enabled;"/> <add name="NormalCompress" connectionString="...;Initial Catalog=NormalCompress;"/> <add name="EncryptCompress" connectionString="...;Initial Catalog=EncryptCompress;Column Encryption Setting=Enabled;"/> </connectionStrings> - tobiからのフィードバック(このコードレビューの質問につながりました)と同僚のBrooke Philpott(@Macromullet)からの多大な支援に基づいて、C#コード(付録を参照)に多くの改善を加えました。これらには以下が含まれます:
- ストアドプロシージャを削除してランダムな名前/給与を生成し、代わりにC#で実行する
Stopwatchを使用する 不器用な日付/時刻文字列の代わりに-
using()のより一貫した使用.Close()の削除 - 少しだけ良い命名規則(およびコメント!)
-
whileを変更するforにループします ループ -
StringBuilderを使用する 単純な連結(最初は意図的に選択したもの)の代わりに - 接続文字列を統合します(ただし、ループの反復ごとに意図的に新しい接続を作成しています)
次に、各テストを5回実行する単純なバッチファイルを作成しました(ローカルコンピューターとリモートコンピューターの両方でこれを繰り返しました):
for /l %%x in (1,1,5) do ( ^ AEDemoConsole "Normal" & ^ AEDemoConsole "Encrypt" & ^ AEDemoConsole "NormalCompress" & ^ AEDemoConsole "EncryptCompress" & ^ )
テストが完了した後、使用された期間とスペースを測定するのは簡単です(そして、結果からグラフを作成するには、Excelで少し操作するだけです):
-- duration
SELECT HostName, Test,
AvgInsertTime = AVG(1.0*InsertTime),
AvgSelectTime = AVG(1.0*SelectTime)
FROM Utility.dbo.Timings
GROUP BY HostName, Test
ORDER BY HostName, Test;
-- space
USE Normal; -- NormalCompress; Encrypt; EncryptCompress;
SELECT COUNT(*)*8.192
FROM sys.dm_db_database_page_allocations(DB_ID(),
OBJECT_ID(N'dbo.Employees'), NULL, NULL, N'LIMITED'); 期間の結果
上記の期間クエリの生の結果は次のとおりです(CANUCK SQL Serverのインスタンスをホストするマシンの名前であり、HOSER コードのリモートバージョンを実行したマシンです):
明らかに、別の形式で視覚化する方が簡単です。最初のグラフに示されているように、リモートアクセスは挿入の期間に大きな影響を与えました(40%以上の増加)が、圧縮はほとんど影響を与えませんでした。暗号化だけでも、テストカテゴリの期間は約2倍になります:
100,000行を挿入する時間(ミリ秒)
読み取りの場合、圧縮は、暗号化やデータのリモート読み取りよりもパフォーマンスに大きな影響を及ぼしました。
100個のランダムな行を1,000回読み取る時間(ミリ秒)
スペースの結果
ご想像のとおり、圧縮によってこのデータの保存に必要なスペースの量が大幅に削減されます(約半分)が、暗号化はデータサイズに反対方向(ほぼ3倍)の影響を与えることがわかります。そしてもちろん、暗号化された値を圧縮しても効果はありません:
圧縮の有無にかかわらず100,000行を格納するために使用されるスペース(KB)暗号化
概要
これにより、AlwaysEncryptedを実装するときにどのような影響が予想されるかを大まかに把握できます。ただし、これは非常に特殊なテストであり、初期のCTPビルドを使用していたことを覚えておいてください。データとアクセスパターンは非常に異なる結果をもたらす可能性があり、将来のCTPのさらなる進歩と.NET Frameworkの更新により、このテストでもこれらの違いの一部が軽減される可能性があります。
また、ここでの結果は、以前の投稿とは全体的にわずかに異なっていることに気付くでしょう。これは説明できます:
- ランダムな名前と給与を生成するためにデータベースへの余分なラウンドトリップが発生しなくなったため、すべてのケースで挿入時間が短縮されました。
- 文字列連結のずさんな方法(期間メトリックの一部として含まれている)を使用しなくなったため、すべての場合で選択時間が短縮されました。
- どちらの場合も、生成されたランダムな文字列の分布が異なるため、使用されるスペースはわずかに大きかったと思います。
付録A– C#コンソールアプリケーションコード
using System;
using System.Configuration;
using System.Text;
using System.Data;
using System.Data.SqlClient;
namespace AEDemo
{
class AEDemo
{
static void Main(string[] args)
{
// set up a stopwatch to time each portion of the code
var timer = System.Diagnostics.Stopwatch.StartNew();
// random object to furnish random names/salaries
var random = new Random();
// connect based on command-line argument
var connectionString = ConfigurationManager.ConnectionStrings[args[0]].ToString();
using (var sqlConnection = new SqlConnection(connectionString))
{
// this simply truncates the table, which I was previously doing manually
using (var sqlCommand = new SqlCommand("dbo.Cleanup", sqlConnection))
{
sqlConnection.Open();
sqlCommand.ExecuteNonQuery();
}
}
// first, generate 100,000 name/salary pairs and insert them
for (int i = 1; i <= 100000; i++)
{
// random salary between 32750 and 197500
var randomSalary = random.Next(32750, 197500);
// random string of random number of characters
var length = random.Next(1, 32);
char[] randomCharArray = new char[length];
for (int byteOffset = 0; byteOffset < length; byteOffset++)
{
randomCharArray[byteOffset] = (char)random.Next(65, 90); // A-Z
}
var randomName = new string(randomCharArray);
// this stored procedure accepts name and salary and writes them to table
// in the databases with encryption enabled, SqlClient encrypts here
// so in a trace you would see @LastName = 0xAE4C12..., @Salary = 0x12EA32...
using (var sqlConnection = new SqlConnection(connectionString))
{
using (var sqlCommand = new SqlCommand("dbo.AddEmployee", sqlConnection))
{
sqlCommand.CommandType = CommandType.StoredProcedure;
sqlCommand.Parameters.Add("@LastName", SqlDbType.NVarChar, 32).Value = randomName;
sqlCommand.Parameters.Add("@Salary", SqlDbType.Int).Value = randomSalary;
sqlConnection.Open();
sqlCommand.ExecuteNonQuery();
}
}
}
// capture the timings
timer.Stop();
var timeInsert = timer.ElapsedMilliseconds;
timer.Reset();
timer.Start();
var placeHolder = new StringBuilder();
for (int i = 1; i <= 1000; i++)
{
using (var sqlConnection = new SqlConnection(connectionString))
{
// loop through and pull 100 rows, 1,000 times
using (var sqlCommand = new SqlCommand("dbo.RetrieveRandomEmployees", sqlConnection))
{
sqlCommand.CommandType = CommandType.StoredProcedure;
sqlConnection.Open();
using (var sqlDataReader = sqlCommand.ExecuteReader())
{
while (sqlDataReader.Read())
{
// do something tangible with the output
placeHolder.Append(sqlDataReader[0].ToString());
}
}
}
}
}
// capture timings again, write both to db
timer.Stop();
var timeSelect = timer.ElapsedMilliseconds;
using (var sqlConnection = new SqlConnection(connectionString))
{
using (var sqlCommand = new SqlCommand("Utility.dbo.AddTiming", sqlConnection))
{
sqlCommand.CommandType = CommandType.StoredProcedure;
sqlCommand.Parameters.Add("@Test", SqlDbType.NVarChar, 32).Value = args[0];
sqlCommand.Parameters.Add("@InsertTime", SqlDbType.Int).Value = timeInsert;
sqlCommand.Parameters.Add("@SelectTime", SqlDbType.Int).Value = timeSelect;
sqlConnection.Open();
sqlCommand.ExecuteNonQuery();
}
}
}
}
}