Amazon Web Services ブログ

SPARQL explain を使用して、Amazon Neptune のクエリ実行を理解する



お客様は、AWS 内で使用するサービスの可視性と制御の向上を引き続き求めています。データベースサービスに関しては、お客様からは通常、特定のデータベース内でのクエリの最適化と処理に関する洞察を求めるリクエストを中心に受けています。データベースの開発者と管理者は、ほとんどの場合、データベースクエリ実行プランのアイデアと使用法をすでによく知っています。お客様の議論に動機付けられて、Amazon Neptune に SPARQL クエリの explain 機能が追加されました。

Amazon Neptune は、高度に連結されたデータの保存とクエリのために最適化された、高速で信頼性に優れた完全マネージド型のグラフデータベースで、データ内の接続のナビゲートと活用に依存するオンラインアプリケーションに最適です。

Amazon Neptune は、SPARQL クエリ言語を使用してクエリできる W3C Resource Description Framework (RDF) グラフをサポートしています。また、Gremlin グラフトラバーサルおよびクエリ言語を使用してクエリできる Apache TinkerPop プロパティグラフもサポートしています。

このブログ記事では、新しい SPARQL explain 機能とその使用方法について詳しく説明します。また、この記事の最後に、今日 SPARQL explain を試してみたい人のために、ワークロードと設定の例を示しました。

explain を使用した SPARQL クエリのランタイム動作の理解

SPARQL クエリが Neptune クラスターに送信されると、データベースエンジンはクエリを SPARQL クエリオプティマイザーに転送します。これにより、利用可能な統計とヒューリスティックに基づいてクエリプランが生成されます。オプティマイザーは、個々のトリプルパターンと接続演算子によってクエリを分割し、最適な実行を提供するために自動的に並べ替えます。このタイプの最適化により、クエリ開発者はクエリを評価する最適な順序を考慮する必要がなくなります。

場合によっては、オプティマイザーが選択したトリプルパターン (より一般的には実行プラン) の評価順序についてより多くの洞察を得たい場合があります。ここで、新しい SPARQL explain 機能の出番です。生成された評価プランを検査して、実行順序を理解できるためです。

クエリ explain 出力は、追加パラメータ「explain=<MODE>」を HTTP リクエストに追加するだけで取得できます。 次の curl コマンド (変数 $NEPTUNE_CLUSTER_ENDPOINT および $NEPTUNE_CLUSTER_PORT は Neptune クラスターのエンドポイントとポートを指す) を使用すると、このクエリを Neptune に送信できます。この場合、SPARQL クエリは query1.sparql というテキストファイルを介して渡されます。

curl -s http://$NEPTUNE_CLUSTER_ENDPOINT:$NEPTUNE_CLUSTER_PORT/sparql?explain=dynamic \
     -d "@query1.sparql" \
     -H "Content-type: application/sparql-query" \
     -H "Accept: text/plain"

 query1.sparql

PREFIX prop: <http://kelvinlawrence.net/air-routes/vocab/prop#> 
PREFIX airport: <http://kelvinlawrence.net/air-routes/data/airport#> 
SELECT DISTINCT ?via WHERE {
  ?route1 prop:from airport:FRA .
  ?route1 prop:to ?via .
  ?route2 prop:from ?via .
  ?route2 prop:to airport:SEA .
}

注: 上記のクエリは、商用航空路を含むデータセットを使用しています。フランクフルト空港からシアトル空港へのワンストップ接続を抽出します。この記事の後半で、クエリの仕組みについて説明します。

HTTP リクエストの最後に「explain=dynamic」を追加すると、次の出力が表示されます。これは、送信された SPARQL クエリと Neptune 内での実行の内訳です。

╔════╤════════╤════════╤═══════════════════╤═══════════════════════════════════════════════════╤══════════╤══════════╤═══════════╤════════╗
║ ID │ Out #1 │ Out #2 │ Name              │ Arguments                                         │ Mode     │ Units In │ Units Out │ Ratio  ║
╠════╪════════╪════════╪═══════════════════╪═══════════════════════════════════════════════════╪══════════╪══════════╪═══════════╪════════╣
║ 0  │ 1      │ -      │ SolutionInjection │ solutions=[{}]                                    │ -        │ 0        │ 1         │ 0.00   ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 1  │ 2      │ -      │ PipelineJoin      │ pattern=distinct(?route2, prop:to, airport:SEA)   │ -        │ 1        │ 118       │ 118.00 ║
║    │        │        │                   │ joinType=join                                     │          │          │           │        ║
║    │        │        │                   │ joinProjectionVars=[?route2]                      │          │          │           │        ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 2  │ 3      │ -      │ PipelineJoin      │ pattern=distinct(?route2, prop:from, ?via)        │ -        │ 118      │ 118       │ 1.00   ║
║    │        │        │                   │ joinType=join                                     │          │          │           │        ║
Project││││joinProjectionVars = [?route2、?via]││││║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 3  │ 4      │ -      │ PipelineJoin      │ pattern=distinct(?route1, prop:to, ?via)          │ -        │ 118      │ 10030     │ 85.00  ║
║    │        │        │                   │ joinType=join                                     │          │          │           │        ║
║    │        │        │                   │ joinProjectionVars=[?route1, ?via]                │          │          │           │        ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 4  │ 5      │ -      │ PipelineJoin      │ pattern=distinct(?route1, prop:from, airport:FRA) │ -        │ 10030    │ 45        │ 0.00   ║
║    │        │        │                   │ joinType=join                                     │          │          │           │        ║
║    │        │        │                   │ joinProjectionVars=[?route1]                      │          │          │           │        ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 5  │ 6      │ -      │ Distinct          │ vars=[?via]                                       │ -        │ 45       │ 45        │ 1.00   ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 6  │ 7      │ -      │ Projection        │ vars=[?via]                                       │ retain   │ 45       │ 45        │ 1.00   ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 7  │ -      │ -      │ TermResolution    │ vars=[?via]                                       │ id2value │ 45       │ 45        │ 1.00   ║
╚════╧════════╧════════╧═══════════════════╧═══════════════════════════════════════════════════╧══════════╧══════════╧═══════════╧════════╝

ヒント: このブログ記事では、常に text/plain を使用します。これにより、ASCII ベースの表形式のシリアル化が生成されます。Neptune は、HTML ベースの出力と CSV シリアル化もサポートしています (詳細な調査のためにスプレッドシートに簡単に貼り付けることができます)。詳細については、ドキュメントを参照してください。

explain パラメータは、static または dynamic に設定できます。前述の例では、説明モード dynamic は、評価の動的な側面、つまりランタイム時にプランを流れるソリューションの数にも関心があることを示しています。static パラメータは、プランの構造を要約した限定ビューを出力します。

SPARQL explain 出力の概要

Neptune クエリプランは、演算子のパイプラインと理解することができます。演算子は、1 つ以上のダウンストリーム演算子からソリューションを受け取り、 (最大 2 つの) アップストリーム演算子にソリューションを転送します。この例は SolutionInjection 演算子で始まり、その後に一連の PipelineJoin 操作が続きます。これはクエリの各トリプルパターンに 1 つずつあり、最後に DistinctProjection、および TermResolution 操作を計算します。列 Out #1 および Out #2 は、演算子が互いにどのように接続されているかを示しています。これらには、指定された演算子が出力を転送する演算子の ID が含まれています。この例では、演算子パイプラインは完全に線形であり、各演算子は次の演算子に転送します。しかし、より複雑なクエリの場合、Neptune は非線形プランを選択する場合があります。この例は Copy 演算子で、SPARQL UNION のさまざまな部分を表すサブプランに結果を転送します。

すべてを分解する

詳細については、上記のプランの仕組みを次に示します (演算子とその引数の詳細については、「SPARQL explain 演算子のリファレンス」を参照してください)。explain 出力の各行は、左から 1 列目に含まれる実行 ID によって参照します。

演算子 ID 0

クエリ評価の最初のステップは、SolutionInjection の手順です。この例では、この手順により、単一のいわゆる「ユニバーサル」ソリューション {} が挿入され、評価プロセス全体で最終結果に拡張されます。この場合、このステップはあまり情報を提供しません。静的ソリューションを注入しないことを示しているだけです。たとえば、SPARQL クエリに、最初に変数バインディングを注入する外側の VALUES 句が含まれる場合、これを行います。Units In および Units Out 列で示されているように、このステップには入力がなく、単一の汎用ソリューションを転送します。

演算子 ID 1〜4

次の 4 つの手順は、いわゆる PipelineJoin 演算子です。これらは、クエリ内のトリプルパターンが評価される順序を反映しています。ここで最も重要なのは、結合によって評価されるトリプルパターンを示す pattern 引数です。たとえば、最初の演算子はトリプルパターン (?route2, prop:to, airport:FRA) を評価し、2 番目のトリプルパターン (?route2, prop:from, ?via) を評価し、と続いていきます。すべての場合において、パターンの先頭には distinct が付いており、明確なソリューションのみに関心があることを示しています。

注: このdistinct の実装では、SPARQL のデフォルトのグラフセマンティクスを使用します。この例では関連していませんが、同じ一致するトリプルが複数の名前付きグラフに存在する可能性があります。その場合、1 つのソリューションのみを抽出します (distinct で示されているとおり)。詳細についてはNeptune が名前付きグラフとデフォルトグラフを処理する方法に関するドキュメントを参照してください。

クエリで指定されているとおりに PipelineJoin のシーケンスをトリプルパターンの順序と比較すると、オプティマイザーが評価順序を変更したことがわかります。クエリプランで選択したアプローチは、目的地のシアトルから開始し、グラフを「後方」に通過し、中継地を経由して出発地のフランクフルト空港にたどり着きます。直観的に言えば、この並べ替えの理由は、シアトルが (接続の点で) 小さい方の空港であるため、シアトルから開始してグラフを走査すると、検索スペースが小さくなるためです。

Neptune の PipelineJoin について知っておくべき重要なことの 1 つは、以前の演算子から知られている変数バインディングが、後続の操作のためにトリプルパターンに「置換」されることです。例に従って:

  1. 演算子 #1: 最初の PipelineJoin は、変数がバインドされていないユニバーサルソリューションを入力として受け取ります。既知の変数バインディングが存在しない場合、データベース内のパターン (?route2, prop:to, airport:SEA) に一致するすべての (distinct) トリプルを単純に検索します。この操作のため、変数 ?route2 は、ターゲットとして airport:SEA を持つすべてのルートにバインドされます。
    explain の Units Out 列は、そのようなルートが 118 あることを示しています。PipelineJoin 演算子は、これらのすべてのルート (?route2 変数のバインディングによって表される) を後続の演算子に転送します。
  1. 演算子 #2: 次の演算子はトリプルパターン (?route2, prop:from, ?via) を実装します。Units In 列は、前のステップで見つかったさまざまな ?route2 バインディングを表す 118 のバインディングが流入していることを示しています。これらのバインディングからの変数は、トリプルパターンに連続して置換されるため、指定された ?route2 が開始された ?via 空港を抽出します。ルートが 2 つの空港間の単一の接続を表すことを考えると、この演算子から 118 のソリューションが流出しています。これらの各ソリューションでは、?via 変数が候補中継空港にバインドされています。
  2. 演算子 #3: パターン (?route1, prop:to, ?via) を評価し、前のパターンでバインドされた空港 ?via で終わるすべてのルート ?route1 を探します。118 の ?via 空港について、このようなルートが 10003 あることがわかります。これは、これらの各空港に平均で約 85 の到着接続空港があることを示しています。
  3. 演算子 #4: 最後の PipelineJoin はトリプルパターンを実装しています (?route1, prop:from, airport:FRA) 演算子に流れている入力では、変数 ?route1 が最近バインドされました。いつものように、これらの候補に置き換えて、完全にバインドされたトリプルパターンのシーケンスを評価します。このような完全にバインドされたパターンの評価では新しいバインディングは導入されませんが、その評価パターンは、トリプルパターンがデータベースに存在するソリューションの「フィルタリング」として理解できます。この場合、実際にフランクフルトから開始したものを保持するために、候補バインディングのセットをフィルタリングします。ここまでは、グラフをさかのぼって調べて、途中で 1 回だけシアトルに到着できるすべての空港を取得していました。当然のことながら、このフィルター制約を適用することにより、演算子は中間ソリューションのサイズを 10030 Units In から 45 Units Out に縮小します。

演算子 ID 5〜6

ここでは、指定された出力変数 ?viaDistinct (演算子番号 5) および Projection (演算子番号 6) を計算します。Distinct 演算子は、基本的にソリューションから他のすべての変数を削除し (PipelineJoin シーケンス全体で収集されているため)、?via 変数で見つかった distinct 値のみを保持します。

演算子 ID 7

パフォーマンスのために、評価プロセス全体を通して、Neptune は用語 (URI や文字列リテラルなど) の内部識別子を操作します。最後の TermResolution 演算子は、これらの内部識別子から辞書編集形式へのマッピングを実行します。通常の (非説明モード) クエリ評価では、この最終的な変換の結果がリクエストされたシリアル化形式にシリアル化され、クライアントにストリーミングされます。

例による最適化: DISTINCT の影響を理解する

クエリプランからすぐに明らかになる興味深い点の 1 つは、Distinct 演算子が効果的に作業を行わず、45 のソリューションが流入し、同じ数のソリューションが流出することです。この例を見ると、世界中の利用可能なすべての商用航空路を含むオープンソースデータセットを使用しています。空港とそれらの間の対応するルートを含むグラフモデルが含まれています。これは、SPARQL explain 機能の出力を示すために以前に実行したのと同じクエリ (query1.sparql) です。

query1.sparql

PREFIX prop: <http://kelvinlawrence.net/air-routes/vocab/prop#> 
PREFIX airport: <http://kelvinlawrence.net/air-routes/data/airport#> 
SELECT DISTINCT ?via WHERE {
  ?route1 prop:from airport:FRA .
  ?route1 prop:to ?via .
  ?route2 prop:from ?via .
  ?route2 prop:to airport:SEA .
}

航空路データの基本的なスキーマを、2 つの直接接続された空港、Seattle TacomaFrankfurt am Main Airport の例により、以下の図に示します。この場合、2 つの空港は飛行ルート (ここではノード「5090」で識別) を介して接続されています。各空港は、コード、都市、緯度、経度、空港のある国などの多くのプロパティによって記述されます。

クエリは、シアトルから始まり、変数 ?via で識別される中継地で終わるルート ?route1 と、中継地 ?via から始まり、フランクフルトで終わるルート ?route2 をチェックします。

AirRoutes データセットでクエリを実行すると、クエリは 45 の空港識別子をレポートします。これは、フランクフルトからシアトルまで 1 回のストップで到達する 45 のすべての方法を反映しています。AirRoutes データセットに実装された URI スキームでは、次のように、識別子の最後の 3 文字が空港コードを表します。

via

… (さらに 40)

この例では、接続された 2 つの空港は常に 1 つのルートのみで接続されています。これは、SPARQL クエリに明示的な DISTINCT 演算子がなくても、クエリが設計により DISTINCT の結果を生成することを意味します。これは、SPARQL クエリで DISTINCT 演算子を削除して、計算に必要な時間と必要なメモリを節約するために役立つ情報です。DISTINCT なしのクエリの explain 出力を以下見てみましょう。

query2.sparql

PREFIX prop: <http://kelvinlawrence.net/air-routes/vocab/prop#> 
PREFIX airport: <http://kelvinlawrence.net/air-routes/data/airport#> 
SELECT ?via WHERE {
  ?route1 prop:from airport:FRA .
  ?route1 prop:to ?via .
  ?route2 prop:from ?via .
  ?route2 prop:to airport:SEA .
}

予想どおり、プランでは Distinct 演算子が省略されているため、次のように作業が少し少なくなります。

curl -s http://$NEPTUNE_CLUSTER_ENDPOINT:$NEPTUNE_CLUSTER_PORT/sparql?explain=dynamic \
     -d "@query2.sparql" \
     -H "Content-type: application/sparql-query" \
     -H "Accept: text/csv"        
╔════╤════════╤════════╤═══════════════════╤═══════════════════════════════════════════════════╤══════════╤══════════╤═══════════╤════════╗
║ ID │ Out #1 │ Out #2 │ Name              │ Arguments                                         │ Mode     │ Units In │ Units Out │ Ratio  ║
╠════╪════════╪════════╪═══════════════════╪═══════════════════════════════════════════════════╪══════════╪══════════╪═══════════╪════════╣
║ 0  │ 1      │ -      │ SolutionInjection │ solutions=[{}]                                    │ -        │ 0        │ 1         │ 0.00   ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 1  │ 2      │ -      │ PipelineJoin      │ pattern=distinct(?route2, prop:to, airport:SEA)   │ -        │ 1        │ 118       │ 118.00 ║
║    │        │        │                   │ joinType=join                                     │          │          │           │        ║
║    │        │        │                   │ joinProjectionVars=[?route2]                      │          │          │           │        ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 2  │ 3      │ -      │ PipelineJoin      │ pattern=distinct(?route2, prop:from, ?via)        │ -        │ 118      │ 118       │ 1.00   ║
║    │        │        │                   │ joinType=join                                     │          │          │           │        ║
Project││││joinProjectionVars = [?route2、?via]││││║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 3  │ 4      │ -      │ PipelineJoin      │ pattern=distinct(?route1, prop:to, ?via)          │ -        │ 118      │ 10030     │ 85.00  ║
║    │        │        │                   │ joinType=join                                     │          │          │           │        ║
║    │        │        │                   │ joinProjectionVars=[?route1, ?via]                │          │          │           │        ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 4  │ 5      │ -      │ PipelineJoin      │ pattern=distinct(?route1, prop:from, airport:FRA) │ -        │ 10030    │ 45        │ 0.00   ║
║    │        │        │                   │ joinType=join                                     │          │          │           │        ║
║    │        │        │                   │ joinProjectionVars=[?route1]                      │          │          │           │        ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 5  │ 6      │ -      │ Projection        │ vars=[?via]                                       │ retain   │ 45       │ 45        │ 1.00   ║
╟────┼────────┼────────┼───────────────────┼───────────────────────────────────────────────────┼──────────┼──────────┼───────────┼────────╢
║ 6  │ -      │ -      │ TermResolution    │ vars=[?via]                                       │ id2value │ 45       │ 45        │ 1.00   ║
╚════╧════════╧════════╧═══════════════════╧═══════════════════════════════════════════════════╧══════════╧══════════╧═══════════╧════════╝

SPARQL explain とオープンソース航空路データセットの使用を開始する方法

このブログ記事では、オープンソース航空路データセットに対するサンプルクエリを使用して、新しい SPARQL explain 機能を使用する方法を説明しました。このデータセットを、Neptune がバルクロードでサポートする一般的な Turtles ファイルシリアル化形式に変換しました。

このブログ記事の例を使って練習したい場合は、Neptune のデータローダーを使用して Neptune インスタンスのいずれかにデータセットをロードするか、さらに便利な方法として、Neptune クラスターと Amazon SageMaker Notebook インスタンスを含む小さなスタックを作成する AWS CloudFormation スタックを準備しました。Notebook インスタンスには、前の記事で説明したすべてのクエリ例が含まれる Jupyter ノートブックが含まれています。スタックの実行中は、インフラストラクチャにコストがかかることに注意してください。スタックをセットアップするには、次の手順に従います。

  1. ブラウザーを開き、http://console.aws.amazon.com にアクセスします。AWS アカウントにログインします。
  2. ログインしたら、下の [スタックを起動] ボタンのいずれかをクリックします。これらはリージョン固有であり、以下の中から選択したリージョンでこのサンプル CloudFormation スタックを起動します。

リージョン

表示 起動

米国東部 1
(バージニア北部)

表示

米国東部 2
(オハイオ)

表示

米国西部 2
(オレゴン)

表示

欧州西部 1
(アイルランド)

表示

欧州西部 2
(ロンドン)

表示

欧州中部 1
(フランクフルト)
表示

  1. 上記の [Launch Stack] リンクには、インポートする Jupyter ノートブックの場所とともに、スタックのタイトルを「SPARQL-Explain」として自動入力するパラメータが含まれています。
    • ページの下部にある 2 つのボックスをチェックして、AWS CloudFormation が IAM リソースを作成し、ネストされたスタックを実行できるようにしてください。
    • [スタックを作成] をクリックします。これにより、新しい Neptune インスタンス (r4.xlarge)、Neptune インスタンスに関連付けられた Jupyter ノートブックを含む Amazon SageMaker Notebook インスタンス、および必要なインフラストラクチャ (VPC、データローダーロールなど) が作成されます。
  2. スタックの準備ができたら、AWS CloudFormation コンソール内でスタックの出力を参照します。
    • SageMakerNotebook を見つけます。これは Amazon SageMaker Notebook インスタンスへのリンクです。このリンクをクリックして、このインスタンスの Jupyter インターフェイスを開きます。

  1. Jupyter がロードされると、Neptune という単一のディレクトリが表示されます。
    • Neptune ディレクトリをクリックしてから、sparqlexplain をクリックします。これには、SPARQLExplainAndQueryHints.ipynb という単一の Jupyter ノートブックが含まれているはずです。
    • このノートブックファイルをクリックして開きます。

  1. ノートブックが開いたら、上部の「再生」ボタンを使用してセルを順番に実行することにより、サンプルを確認できます。サンプルを変更する場合は、既存のセルを変更 (または新しいセルを作成) してから再実行するなどの方法があります。

再生し終わったら、デプロイされたインフラストラクチャリソースをクリーンアップするために、AWS CloudFormation スタックを終了することを忘れないでください。これを行うには、CloudFormation コンソールに戻って、ルートスタック (SPARQL-Explain と呼ばれる) を選択し、[削除] ボタンをクリックします。

まとめ

SPARQL explain は、Neptune のランタイム動作を理解および調整するための優れたツールです。次の場合に役立ちます。

  • クエリの動作、たとえばクエリが実際に評価が難しい理由を理解する
  • クエリの改善されたバージョンを作成する場合。たとえば、上記の例の DISTINCT などの冗長な演算子を削除することにより、
  • 時空間のトレードオフを特定する
  • または、手動で調整したプランがオプティマイザーによって生成されたプランを改善する可能性があるシナリオに注意してください。

特に、SPARQL クエリテンプレートのセットに基づいて Neptune の上に高レベルのグラフアプリケーションを構築する場合、SPARQL explain を介してテンプレートの評価動作を理解および調整すると、パフォーマンスがさらに向上する可能性があります。これにより、最終的にアプリケーションのエンドユーザーのユーザーエクスペリエンスが向上します。

SPARQL explain をさらに深く掘り下げてみると、サンプルはほんの触りに過ぎないことがわかります。公式の説明ドキュメントには、すべての演算子と可能な引数の完全なリファレンスが含まれています。いつものように、AWS はフィードバックを歓迎します。コメントや質問を以下に残してください。

 


著者について

 

Taylor Riggan はシニアスペシャリストソリューションアーキテクトで、アマゾン ウェブ サービスでグラフおよびインメモリデータベースを担当しています。 彼はあらゆる規模のお客様と協力して、独自の NoSQL データベースの学習と使用を支援しています。

 

 

 

Michael Schmidt はシニアアマゾン ウェブ サービスのシニアソフトウェア開発エンジニアです。 彼は、Neptune のクエリ最適化スタックのリードアーキテクトとして、AWS のお客様と社内開発チームの両方と連携して、Amazon Neptune のパフォーマンス、スケーラビリティ、ユーザーエクスペリエンスを向上させています。