2026/04/17

(PC) 現場で笑顔になるためのSQL-Server実践ガイドをWebサイト公開しました

こんにちは!!もりもりです。

昨日、かなり頑張って作った *** 現場で笑顔になるためのSQL-Server実践ガイドですが、ブログ記事だとデカすぎて厳しいため、Webサイト版も作りました!!

SQL-ServerのガイドブックをWeb公開



SQL-Serverに限らないと思いますが、DB系は、学習も半ばでいきなり現場に投入され、激しい応用から無理やりやらされて熟練者にぶっ叩かれて苦手になってしまうという地獄のようなパターンが多いのではないかと実感してます。

私自身も地面にはいつくばって土を食うみたいな目に散々あっています。

そんな中、現場で笑顔になれるといいなという願いを込めたガイドブックです。

▶ 「ブック」カテゴリーのトップページはこちらです

▶ 直接はこちらです


まだまだ夜明けは遠いかもしれませんが、それでも明けない夜はないという名言もございます。

私もまだ夜の闇の中を走り続けている身ですが、皆さんと一緒に夜明けまで走りたいです!!

それではまた次の記事でお会いしましょう!!

2026/04/16

(BOOK) 【SQL Server】現場で笑顔になるための応用ガイドブック

こんにちは!!もりもりです!!

今回は応用的なデータベース活用、【SQL Server】現場で笑顔になるための応用ガイドブックをお届けします!!


【SQL Server】現場で笑顔になるための応用ガイドブック

📚 本ガイドブックは、***SQL Server*** をすでに使っている方、または開発現場にいきなり投入された方に向けた「あやふやをなくし、やってはいけないことを知る」ための実践書です。単なる機能説明ではなく、***知らないと痛い目に遭う落とし穴***と、***ゲーム開発並みのチューニング思想***を軸に解説します。あくまで個人環境での自己学習の結果をガイドブックにしていますので、サンプルはあくまでも雰囲気を見る用として捉えていただければ幸いです。

---

目次

1. [実行環境と前提条件](#実行環境と前提条件)

2. [インデックスについて](#インデックスについて)

3. [トランザクションについて](#トランザクションについて)

4. [tempdb について](#tempdb-について)

5. [テーブル名の接頭辞 `#` と `##` について](#テーブル名の接頭辞--と--について)

6. [鬼門のサーバーまたがりのクエリーについて](#鬼門のサーバーまたがりのクエリーについて)

7. [コラム:MSDTCは「素のネットワーク」を愛する](#コラムmsdtcは素のネットワークを愛する)

8. [分離レベルと `WITH(NOLOCK)` の罠](#分離レベルと-withnolockの罠)

9. [よくある質問とトラブルシューティング](#よくある質問とトラブルシューティング)

10. [SQL Server チューニングの現実と未来](#sql-server-チューニングの現実と未来)

---

実行環境と前提条件

本ガイドブックのサンプルコードは以下を前提とします。

項目前提条件
***SQL Server バージョン***SQL Server 2016 以上(一部 2019 以上の機能を明記)
***データベース***`TestDB` という名前のデータベースが存在すること
***テーブル***後述のサンプルDDLで作成した `Orders`・`Accounts`・`Customers` テーブル
***ツール***SSMS(SQL Server Management Studio)または Azure Data Studio

サンプルテーブルDDL

本ガイドブック全体を通して使用するサンプルテーブルです。まずこれを作成してからサンプルを実行してください。



USE TestDB;

GO

# Customers テーブル

CREATE TABLE Customers (

CustomerId INT IDENTITY(1,1) PRIMARY KEY,

Name NVARCHAR(100) NOT NULL,

Email NVARCHAR(200) NOT NULL

);

# Orders テーブル(主要なサンプル対象)

CREATE TABLE Orders (

OrderId INT IDENTITY(1,1) PRIMARY KEY,

OrderDate DATE NOT NULL,

CustomerId INT NOT NULL,

Amount DECIMAL(10, 2) NOT NULL,

Status NVARCHAR(50) NOT NULL DEFAULT 'New',

CONSTRAINT FK_Orders_Customers FOREIGN KEY (CustomerId) REFERENCES Customers(CustomerId)

);

# Accounts テーブル(トランザクションサンプル用)

CREATE TABLE Accounts (

AccountId INT IDENTITY(1,1) PRIMARY KEY,

Balance DECIMAL(18, 2) NOT NULL DEFAULT 0

);

# 動作確認用のサンプルデータ

INSERT INTO Customers (Name, Email)

VALUES (N'山田太郎', N'yamada@example.com'),

(N'佐藤花子', N'sato@example.com');

INSERT INTO Orders (OrderDate, CustomerId, Amount, Status)

VALUES ('2025-01-01', 1, 1000.00, 'New'),

('2025-01-15', 2, 2500.00, 'Shipped'),

('2025-02-01', 1, 800.00, 'New');

INSERT INTO Accounts (Balance)

VALUES (100000), (50000);

GO







> ***⚠️ 注意***:本番データベースでは上記DDLを実行しないでください。必ず開発・検証用の環境で使用してください。 ---

インデックスについて

インデックスとは何か

***インデックス(Index)*** とは、テーブル上のデータを高速に検索するための「索引」です。書籍の巻末索引と同じ仕組みで、特定の列の値に対してどのページ(ページ=SQL Serverのデータ格納単位)にデータがあるかを記録しています。

インデックスがない状態で検索を行うと、SQL Server はテーブルの全ページを先頭から末尾まで順番に読む ***テーブルスキャン(Table Scan)*** を行います。データ件数が少ないうちは問題になりませんが、数百万件・数千万件のテーブルになると、これが致命的なボトルネックになります。

> ***なぜインデックスが重要なのか***:ゲームのロードが遅い原因の大半がディスクのランダムアクセスであるように、SQLの遅さの大半は「どこを読むべきかわからない」ことによる全ページ読み込みです。インデックスはその「カーナビ」の役割を果たします。 ---

インデックスの種類と使用するシーン

SQL Server には複数のインデックス種類があります。種類を誤ると、むしろパフォーマンスが悪化する場合があります。

インデックス種別説明適したシーン
***クラスター化インデックス***データ行そのものを物理的に並び替えて格納主キー(1テーブルに1つのみ)
***非クラスター化インデックス***データとは別に索引を作成検索・絞り込みに使う列
***複合インデックス***複数列を組み合わせたインデックスWHERE句で複数列を組み合わせる検索
***カバリングインデックス***SELECT対象列もインデックスに含めるSELECT列とWHERE列が固定されているクエリ
***フィルターインデックス***WHERE条件を付けて一部のデータのみ対象NULL以外・特定フラグのみを頻繁に検索する場合
***列ストアインデックス***列単位で圧縮格納(DWH向き)集計・分析クエリ(OLAP)
#### 🖥️ ローカルサーバー(自サーバー)での影響

ローカルサーバー上のテーブルへのクエリでは、インデックスが適切に利用されれば***劇的な速度改善***が見込めます。

***実例比較***



# インデックスなし:OrderDate で絞り込む(全件スキャン発生)

SELECT * FROM Orders WHERE OrderDate = '2025-01-01';

# インデックスあり:OrderDate に非クラスター化インデックスを作成済み

CREATE INDEX IX_Orders_OrderDate ON Orders (OrderDate);

SELECT * FROM Orders WHERE OrderDate = '2025-01-01';







***実行計画の確認方法***



# 実行計画を確認する(Ctrl+M または先頭に SET SHOWPLAN_ALL ON を付加)

SET SHOWPLAN_ALL ON;

GO

SELECT * FROM Orders WHERE OrderDate = '2025-01-01';

GO

SET SHOWPLAN_ALL OFF;







***出力例(インデックスなし)***



Table Scan (Object: dbo.Orders) ... Estimated Rows: 1500000







***出力例(インデックスあり)***



Index Seek (IX_Orders_OrderDate) ... Estimated Rows: 120







`Table Scan` → `Index Seek` に変わっていれば、インデックスが使われています。`Index Scan` だった場合は、インデックスは使われているが全体スキャンになっているため、WHERE条件や複合インデックスの見直しが必要です。

#### 🌐 サーバーまたがり(リモートサーバー)クエリでの影響

ここが***最大の落とし穴***です。

リモートサーバー(後述の4部構成名や `OPENQUERY` 等でアクセス)に対してクエリを発行する際、***リモート側のインデックスが必ずしも活用されるとは限りません***

特に4部構成名(`リンクサーバー名.データベース.スキーマ.テーブル`)を使用した場合、SQL Server の最適化エンジンはリモート側のインデックス情報を十分に把握できず、***ローカル側で全件フェッチしてからフィルタリングを行う***という最悪のシナリオが発生することがあります。



# NG例:リモートのテーブルから絞り込み(全件フェッチ後にフィルタが起きる危険性あり)

SELECT * FROM [RemoteServer].[SalesDB].[dbo].[Orders]

WHERE OrderDate = '2025-01-01';







これは、数百万件のデータをネットワーク越しに転送してから WHERE を適用するという、***ネットワーク帯域の大量消費***を引き起こします。

> ***対策***:リモートサーバー側にストアドプロシージャを用意し、絞り込み済みの結果だけをローカルに引き込む(後述の「サーバーまたがりのクエリー」セクションで詳述)。 ---

トランザクションでのインデックスの影響

#### 🖥️ ローカルサーバーでの影響

インデックスはSELECTを高速化しますが、***INSERT / UPDATE / DELETE には逆効果になる***場合があります。

なぜなら、データを変更するたびにインデックスも更新しなければならないからです。インデックスが多いテーブルほど、書き込み処理のコストが高くなります。



# インデックスが多いテーブルへの大量INSERT(遅くなる可能性あり)

INSERT INTO Orders (OrderDate, CustomerId, Amount)

VALUES ('2025-01-01', 1001, 5000.00);

# → Orders テーブルに10個のインデックスがあれば、10個全部を更新する







> ***チューニングの方針***:バッチ処理(大量INSERT/UPDATE)を行う前にインデックスを一時的に無効化し、処理が終わったら再構築するアプローチが有効です。


# インデックスを無効化

ALTER INDEX IX_Orders_OrderDate ON Orders DISABLE;

# 大量INSERTを実行(例:別テーブルからのデータ移行)

INSERT INTO Orders (OrderDate, CustomerId, Amount, Status)

SELECT OrderDate, CustomerId, Amount, Status

FROM SourceOrders

WHERE MigrationFlag = 1;

# インデックスを再構築

ALTER INDEX IX_Orders_OrderDate ON Orders REBUILD;







> ***⚠️ 注意***:`DISABLE` 中はそのインデックスを使うクエリが実行できません(クラスター化インデックスを無効化するとテーブル全体がアクセス不能になります)。***クラスター化インデックスは絶対に DISABLE しないでください***。 #### 🌐 サーバーまたがりのトランザクション中でのインデックスの影響

リモートサーバーを跨いだトランザクション(分散トランザクション)のなかでリモートテーブルへのアクセスが発生すると、インデックスが使われているかどうかに関係なく、***ロックがリモートサーバー上に長時間残留する***可能性があります。これがデッドロックや著しいパフォーマンス低下の原因になります。

> ***後述の「サーバーまたがりのトランザクション」セクションで詳しく説明します。*** ---

このように使ってはいけない(インデックスのNG集)

#### ❌ NG①:インデックス列を関数で加工してWHEREに使う



# ❌ インデックスが無効化される!

SELECT * FROM Orders WHERE YEAR(OrderDate) = 2025;

# ✅ 範囲指定に書き直すとインデックスが使われる

SELECT * FROM Orders WHERE OrderDate >= '2025-01-01' AND OrderDate < '2026-01-01';







***理由***:`YEAR()` のような関数を列に適用すると、SQL Server はインデックスを使って絞り込むことができず、全件スキャンに戻ります。これを「SARGable でない条件」と言います(SARGable = Search ARGument ABLE)。

#### ❌ NG②:LIKE の先頭ワイルドカード



# ❌ 前方ワイルドカードはインデックスが使えない

SELECT * FROM Customers WHERE Name LIKE '%山田%';

# ✅ 前方一致ならインデックスが使われる

SELECT * FROM Customers WHERE Name LIKE '山田%';







#### ❌ NG③:インデックスを作りすぎる

SELECT だけを高速化しようとしてインデックスを乱造すると、INSERT / UPDATE / DELETE が著しく遅くなります。また、インデックスはディスクスペースも消費します。***目安は1テーブル5〜7個以下***を意識してください。

> ***💡 コラム:使われていないインデックスを発見する*** > > 運用中のサーバーに「本当に使われているのか分からないインデックス」が積み重なっていることは珍しくありません。以下のDMVクエリで、SQL Server 起動後に一度も使われていないインデックスを洗い出せます。 > >


> -- 使われていない(またはほとんど使われていない)インデックスを確認 > SELECT > OBJECT_NAME(i.object_id) AS table_name, > i.name AS index_name, > i.type_desc, > ISNULL(u.user_seeks, 0) AS user_seeks, > ISNULL(u.user_scans, 0) AS user_scans, > ISNULL(u.user_lookups, 0) AS user_lookups, > ISNULL(u.user_updates, 0) AS user_updates -- 更新のたびにこのインデックスを維持しているコスト > FROM sys.indexes i > LEFT JOIN sys.dm_db_index_usage_stats u > ON i.object_id = u.object_id > AND i.index_id = u.index_id > AND u.database_id = DB_ID() > WHERE i.object_id > 100 -- システムテーブルを除外 > AND i.type_desc = 'NONCLUSTERED' > AND (u.user_seeks IS NULL OR u.user_seeks + u.user_scans + u.user_lookups = 0) > ORDER BY ISNULL(u.user_updates, 0) DESC; >






> > `user_seeks / user_scans / user_lookups` がすべて 0 なのに `user_updates` が大きい場合、そのインデックスは「読まれずに更新コストだけ払っている」状態です。削除の候補になります。 > > ⚠️ ただし、このDMVは SQL Server の再起動でリセットされます。再起動後間もない時期の結果で判断しないようにしてください。 #### ❌ NG④:断片化放置

インデックスは時間の経過とともに断片化(フラグメンテーション)します。断片化が進むと、インデックスがあっても遅くなります。定期的なメンテナンスを怠らないようにしてください。



# インデックス断片化の確認

SELECT

index_id,

avg_fragmentation_in_percent,

page_count

FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('Orders'), NULL, NULL, 'LIMITED');

# 断片化が30%未満 → REORGANIZE(オンラインで実行可能)

ALTER INDEX IX_Orders_OrderDate ON Orders REORGANIZE;

# 断片化が30%以上 → REBUILD(ロック発生・オフラインが原則)

ALTER INDEX IX_Orders_OrderDate ON Orders REBUILD;







---

トランザクションについて

トランザクションとは何か?

***トランザクション(Transaction)*** とは、「複数の処理をひとまとまりとして扱い、全部成功するか全部失敗するかのどちらかにする」仕組みです。

代表例として「銀行振込」が挙げられます。「A口座から引き落とす」と「B口座に入金する」は、どちらか一方だけ成功するとお金が消えたり増えたりします。この2つは必ずセットで成功・失敗しなければなりません。これがトランザクションの本質です。

SQL Serverでは、この整合性を ***ACID特性*** と呼びます:

特性正式名称意味
***A***Atomicity(原子性)全部成功か全部失敗か
***C***Consistency(一貫性)常にデータの整合性が保たれる
***I***Isolation(独立性)他のトランザクションの影響を受けない
***D***Durability(永続性)コミット済みのデータは消えない
---

SQL Server のトランザクションについて

SQL Server のトランザクションには3種類あります:

種類特徴用途
***自動コミットトランザクション***各DML文が1つのトランザクションとして即座にコミットされる***SQL Server のデフォルト動作***。単独のINSERT/UPDATE/DELETE
***明示的トランザクション***`BEGIN TRAN` ~ `COMMIT / ROLLBACK` で明示的に制御複数の処理をひとまとまりにしたい場合
***暗黙的トランザクション***`SET IMPLICIT_TRANSACTIONS ON` で有効になる。DML/DDL文の実行開始時に自動で `BEGIN TRAN` が発行され、明示的な `COMMIT` / `ROLLBACK` が必要になるレガシーシステムや特定の互換性要件がある特殊なケース(通常は使わない)
> ***⚠️ よくある誤解***:「暗黙的トランザクション」はSQL Serverのデフォルト動作ではありません。デフォルトは「自動コミット」です。暗黙的トランザクションは`SET IMPLICIT_TRANSACTIONS ON`を明示的に設定した場合のみ有効になります。他のDBMSからの移行者が混乱しやすいポイントです。


# 明示的トランザクションの基本構文

BEGIN TRANSACTION;

UPDATE Accounts SET Balance = Balance - 10000 WHERE AccountId = 1;

UPDATE Accounts SET Balance = Balance + 10000 WHERE AccountId = 2;

# エラーがなければコミット

IF @@ERROR = 0

COMMIT TRANSACTION;

ELSE

ROLLBACK TRANSACTION;







> ***現代的な書き方***:`@@ERROR` よりも `TRY...CATCH` 構文を使うほうが安全で推奨されます:


BEGIN TRANSACTION;

BEGIN TRY

UPDATE Accounts SET Balance = Balance - 10000 WHERE AccountId = 1;

UPDATE Accounts SET Balance = Balance + 10000 WHERE AccountId = 2;

COMMIT TRANSACTION;

END TRY

BEGIN CATCH

# エラー発生時にロールバック

ROLLBACK TRANSACTION;

THROW; -- エラーを呼び出し元に再スロー

END CATCH;







---

ローカルサーバーのトランザクションの動き

ローカルサーバー(自サーバー)内でのトランザクションでは、SQL Server の***ロックマネージャー***がリソースを管理します。

ロックには以下の粒度があります(小→大):

ロック粒度説明
***行ロック(Row Lock)***特定の行だけをロック
***ページロック(Page Lock)***8KBのページ単位でロック
***テーブルロック(Table Lock)***テーブル全体をロック
***データベースロック(DB Lock)***データベース全体をロック
SQL Server は最初は行ロックで始まり、ロック数が多くなると自動的に粒度を上げる ***ロックエスカレーション*** を行います。これが意図せずテーブルロックを引き起こし、他のユーザーを長時間ブロックする原因になります。

***デッドロック発生のメカニズム***



# セッション1

BEGIN TRANSACTION;

UPDATE Orders SET Status = 'Shipped' WHERE OrderId = 1;

# この時点でOrderId=1の行をロック取得

# セッション2(同時実行)

BEGIN TRANSACTION;

UPDATE Orders SET Status = 'Cancelled' WHERE OrderId = 2;

# この時点でOrderId=2の行をロック取得

# セッション1(続き)

UPDATE Orders SET Status = 'Shipped' WHERE OrderId = 2;

# OrderId=2はセッション2がロック中 → 待機

# セッション2(続き)

UPDATE Orders SET Status = 'Cancelled' WHERE OrderId = 1;

# OrderId=1はセッション1がロック中 → 待機

# → お互いが相手を待つ「デッドロック」発生!







SQL Server は約5秒でデッドロックを検知し、片方を「デッドロックの犠牲者」としてロールバックします。

> ***デッドロック防止策***:複数テーブル/行を更新する場合は、***常に同じ順序でアクセスする***ことが基本です。 ---

サーバーまたがり(リモートサーバー)のトランザクションの動き

ここが***最も危険な地帯***です。

ローカルサーバーとリモートサーバーにまたがるトランザクションは「***分散トランザクション***」になります。SQL Server はこれを処理するために ***Microsoft Distributed Transaction Coordinator(MS DTC)*** を使用します。

分散トランザクションは以下の「2相コミット(Two-Phase Commit)」で動作します:



フェーズ1(準備):

→ ローカルサーバー: 「コミットできますか?」

→ リモートサーバー: 「準備OK」

フェーズ2(確定):

→ ローカルサーバー: 「コミットしてください」

→ リモートサーバー: 「コミット完了」







この2段階の確認のために、***ロックがネットワーク往復分だけ長く保持される***ことになります。これはデッドロックのリスクを著しく高めます。

> ***重要な落とし穴***:MS DTC が設定されていない環境では、分散トランザクションが `BEGIN TRANSACTION` のブロック内でリモートサーバーにアクセスした時点で***エラーになります***


エラー メッセージ:

サーバー 'RemoteServer' ではリモート アクセスが許可されていないか、

サービス 'msdtc' が開始されていません。







---

このように使ってはいけない(トランザクションのNG集)

#### ❌ NG①:トランザクションを長くかけすぎる



# ❌ トランザクション内でユーザー入力待ちをするような設計

BEGIN TRANSACTION;

SELECT * FROM Customers; -- この結果をユーザーに見せて、次の操作を待つ

# ← ここで数分間ロックが保持される

UPDATE Orders SET Status = 'Confirmed' WHERE CustomerId = 1;

COMMIT TRANSACTION;







トランザクション中はロックが保持されます。長時間のトランザクションは他のユーザーをブロックし続けます。

#### ❌ NG②:TRY-CATCH の外側でトランザクションを開始する



# ❌ 不完全なエラーハンドリング

BEGIN TRANSACTION;

UPDATE Accounts SET Balance = Balance - 1000 WHERE AccountId = 1;

# ここでエラーが起きてもROLLBACKが呼ばれない

UPDATE Accounts SET Balance = Balance + 1000 WHERE AccountId = 2;

COMMIT;







> ***必ず TRY...CATCH でラップし、CATCH ブロック内で ROLLBACK を行う***ことを徹底してください。 #### ❌ NG③:ネストしたトランザクションを誤解する

SQL Server の `BEGIN TRANSACTION` はネストできますが、***内側の COMMIT は外側のトランザクションを確定しません***



BEGIN TRANSACTION; -- トランザクションカウント: 1

BEGIN TRANSACTION; -- トランザクションカウント: 2

COMMIT TRANSACTION; -- カウントが2→1になるだけ(データは確定していない!)

ROLLBACK TRANSACTION; -- カウントが1→0 → 全部ロールバック!







`@@TRANCOUNT` で現在のネスト深さを確認できます。

#### ❌ NG④:分散トランザクション内での大量データ操作

リモートサーバーへの UPDATE や INSERT を `BEGIN TRANSACTION` の中で大量に行うと、MS DTC のタイムアウトが発生したり、ネットワーク障害時にどちらのサーバーの状態が正となるか不定になる「インダウトトランザクション」が発生します。これは非常に厄介です。

---

tempdb について

tempdb とは何か?

***tempdb*** は、SQL Server インスタンス内の***すべてのデータベースが共有する特殊なシステムデータベース***です。SQL Server が再起動するたびに中身は自動的にリセットされます(永続化されません)。

tempdb が使われる主なシーン:

用途説明
***一時テーブル(`#` / `##`)***セッション/グローバルスコープの一時テーブル
***テーブル変数(`@table`)***内部的に tempdb を使う場合がある
***スピル(Spill to Disk)***ソート・ハッシュ結合などがメモリに収まらない場合の作業領域
***スナップショット分離***RCSI(行バージョン管理)の行バージョン格納
***カーソル***カーソルの作業領域
> ***重要点***:tempdb は全ユーザー・全データベースが共有します。1つのクエリが tempdb を大量に使うと、***他の全ユーザーのクエリが遅くなる***という影響が出ます。 ***tempdb のサイズと空き確認***



# tempdb の現在の使用量を確認

SELECT

DB_NAME(database_id) AS DBName,

type_desc,

name,

size * 8 / 1024 AS size_MB,

CASE max_size

WHEN -1 THEN 'ディスク上限まで使用可(実質的なディスク容量が上限)'

ELSE CAST(max_size * 8 / 1024 AS VARCHAR) + ' MB'

END AS max_size_desc

FROM sys.master_files

WHERE database_id = DB_ID('tempdb');







***出力例***



DBName type_desc name size_MB max_size_desc

tempdb ROWS tempdev 1024 ディスク上限まで使用可(実質的なディスク容量が上限)

tempdb LOG templog 512 ディスク上限まで使用可(実質的なディスク容量が上限)







> ***⚠️ `-1` の意味に注意***:`max_size = -1` は「無制限」ではなく、「ディスクの空き容量がなくなるまで使う」という意味です。つまりディスクが満杯になると SQL Server 全体が停止します。本番環境では最大サイズを明示的に制限することを強く推奨します。 ---

このように使ってはいけない(tempdb のNG集)

#### ❌ NG①:tempdb に大量データを長時間保持する

tempdb は全ユーザー共有のリソースです。大きな一時テーブルを長時間放置すると、他のユーザーに影響します。使い終わったら `DROP TABLE #TempTable;` で明示的に削除してください。



# ❌ 使い終わっても消えるのを「SQL Serverに任せる」設計

CREATE TABLE #WorkData (Id INT, Value NVARCHAR(200));

INSERT INTO #WorkData

SELECT CustomerId, CAST(Amount AS NVARCHAR(200)) FROM Orders;

# 何かの処理

# → セッションが終わるまで tempdb を占有し続ける

# ✅ 使い終わったらすぐに削除

DROP TABLE IF EXISTS #WorkData;







#### ❌ NG②:ループ内で毎回テーブルを作成・削除する



# ❌ ループ内での繰り返しCREATE/DROP(tempdbへの負荷大)

DECLARE @i INT = 1;

WHILE @i <= 10000

BEGIN

CREATE TABLE #Temp (Val INT);

INSERT INTO #Temp VALUES (@i);

# 何かの処理

DROP TABLE #Temp;

SET @i = @i + 1;

END

# ✅ ループの外でTRUNCATE+再利用

DECLARE @i INT = 1;

CREATE TABLE #Temp (Val INT);

WHILE @i <= 10000

BEGIN

TRUNCATE TABLE #Temp;

INSERT INTO #Temp VALUES (@i);

# 何かの処理

SET @i = @i + 1;

END

DROP TABLE #Temp;







#### ❌ NG③:テーブル変数として使ったほうがいい場面で一時テーブルを使う(逆も然り)

比較項目テーブル変数(`@table`)一時テーブル(`#table`)
***統計情報***なし(推定行数が常に1件)あり
***インデックス***作成できない(主キーのみ)追加可能
***適したデータ量***数百件以下の小さなデータ数千件以上のデータ
***スコープ***バッチが終われば消えるセッションが終われば消える
***ログ書き込み***最小限あり(ロールバック可能)
> ***判断基準***:少量データで使い捨てなら `@table`、大量データや複雑な処理には `#table` を選択してください。 #### ❌ NG④:tempdb のファイルが1つしかない(パフォーマンス問題)

SQL Server の tempdb はデフォルトで1ファイルですが、複数ファイルに分割することで***並列アクセスの競合(PFS/GAM/SGAM ページの競合)***を減らすことができます。

> ***推奨設定***:論理CPUコア数に応じて、最大8ファイルまで増やすことが Microsoft の公式推奨です。


# tempdb にデータファイルを追加(4コアの例:4ファイルに)

ALTER DATABASE tempdb

ADD FILE (

NAME = 'tempdev2',

FILENAME = 'D:\SQLData\tempdb2.ndf',

SIZE = 1024MB,

FILEGROWTH = 256MB

);







---

テーブル名の接頭辞 `#` と `##` について

これは何か

SQL Server では、テーブル名の先頭に特殊な記号を付けることで、***格納場所とスコープを変える***ことができます。

記号名称格納先スコープ
`#テーブル名`ローカル一時テーブルtempdb作成したセッションのみ(セッション終了で削除)
`##テーブル名`グローバル一時テーブルtempdb全セッション共有(全員が参照を終えたら削除)
なし永続テーブル通常のデータベース明示的にDROPするまで存在
***使用例***



# ローカル一時テーブル(自分のセッションだけに見える)

CREATE TABLE #MyTemp (Id INT, Name NVARCHAR(100));

INSERT INTO #MyTemp VALUES (1, N'山田太郎');

SELECT * FROM #MyTemp;

# セッション終了(接続が切れる)と自動削除

# グローバル一時テーブル(全セッションから見える)

CREATE TABLE ##SharedTemp (Id INT, Name NVARCHAR(100));

INSERT INTO ##SharedTemp VALUES (1, N'共有データ');

SELECT * FROM ##SharedTemp; -- 別のセッションからも参照できる

# 全セッションが参照を終えると自動削除







> ***内部の話***:`#TempTable` は tempdb 内では `#TempTable___________00000000001A` のようなシステムが付加したサフィックス付きの名前で格納されています。これにより、複数セッションが同名の `#` テーブルを作っても競合しない仕組みになっています。 ---

このように使ってはいけない

#### ❌ NG①:グローバル一時テーブル(`##`)を業務データの受け渡しに使う



# ❌ 危険な使い方:セッション間のデータ共有に ## を使う

CREATE TABLE ##TransferData (Id INT, Status NVARCHAR(50));

# ← セッションAがデータを入れて、セッションBが読む設計

# 問題1:セッションAが切れたら ##TransferData は消える

# 問題2:他の人も同じ名前で作ろうとするとエラーになる

# 問題3:複数のユーザーが同時に使う場合の排他制御がない







***セッション間のデータ共有には永続テーブルやキューテーブルを使ってください。***

#### ❌ NG②:`IF OBJECT_ID('tempdb..#Temp') IS NOT NULL` チェックを忘れる

バッチを再実行する際に、前回の実行で一時テーブルが残っているとエラーになります:



# ❌ 再実行でエラーになるコード

CREATE TABLE #Temp (Id INT); -- 既に存在するとエラー: オブジェクト '#Temp' はすでに存在します

# ✅ 安全なコード(SQL Server 2016以降)

DROP TABLE IF EXISTS #Temp;

CREATE TABLE #Temp (Id INT);

# ✅ SQL Server 2014以前の書き方

IF OBJECT_ID('tempdb..#Temp') IS NOT NULL DROP TABLE #Temp;

CREATE TABLE #Temp (Id INT);







#### ❌ NG③:ストアドプロシージャ内の一時テーブルに統計情報が古い問題

ストアドプロシージャ内で一時テーブルを作成してデータを入れると、最初のコンパイル時の統計情報でプランがキャッシュされます。2回目以降の実行でデータ量が大きく変わっても、古い実行計画が使われ続ける(***パラメータスニッフィング問題***の一時テーブル版)ことがあります。



# 解決策:統計情報を明示的に更新する

DROP TABLE IF EXISTS #TempWork;

CREATE TABLE #TempWork (Id INT, Amount DECIMAL(10, 2));

INSERT INTO #TempWork

SELECT OrderId, Amount FROM Orders WHERE CustomerId = 1;

# 統計情報を更新してからクエリを実行

UPDATE STATISTICS #TempWork;

SELECT o.OrderId, t.Amount

FROM Orders o

JOIN #TempWork t ON o.OrderId = t.Id;







---

鬼門のサーバーまたがりのクエリーについて

サーバーまたがりとは

***サーバーまたがりのクエリー(Cross-Server Query)*** とは、ローカルサーバー(以下「自サーバー」)から、ネットワーク上の別の SQL Server(以下「リモートサーバー」)のデータにアクセスするクエリのことです。

SQL Server でリモートサーバーにアクセスする主な方法は以下の3種類です:

方法形式特性
***4部構成名***`[サーバー].[DB].[スキーマ].[テーブル]`リンクサーバー経由。最も直感的だが危険も多い
***OPENROWSET***`OPENROWSET('SQLNCLI17', '接続文字列', 'クエリ')`アドホック接続。リンクサーバー不要
***OPENQUERY***`OPENQUERY(リンクサーバー名, 'クエリ')`リモート側でクエリを実行してから結果を返す
> ***用語について***:「ローカル」「リモート」が標準的な用語です。以降は自サーバー(ローカル)・リモートサーバー(リモート)と呼びます。 ---

高速なデータ取得のセオリー

#### 🔑 大原則:「データを引き込む」のではなく「加工してから引き込む」

ネットワーク越しのデータ転送がボトルネックになります。リモートの全データをローカルに持ってきてからフィルタをかけるのは最悪のパターンです。

***理想のフロー***



リモートサーバー:ストアドプロシージャで絞り込み・集計

↓ (絞り込み済みの少量データだけがネットワークを流れる)

自サーバー:受け取ったデータを使って以降の処理







#### 💡 私の実践経験から:リモートSTORED PROCEDUREを自サーバーで呼び出す手法について

> 「リモートサーバーに設置したストアドプロシージャから結果を自サーバーに引き込む」という手法。***これが非常に妥当かつ正攻法の手法です***。 この手法の優れた点:

観点評価理由
***パフォーマンス*** 優秀リモート側でフィルタ・集計が完了してから転送されるため、ネットワーク転送量が最小
***インデックス活用*** 優秀リモートのストアドはリモートのインデックスを最大活用できる
***ロック影響*** 良好ストアド単独の実行はMSDTCを使わず、自サーバーのロック影響を最小化
***保守性*** 良好リモート側のロジック変更が自サーバー側に影響しない


# リモートのストアドプロシージャを呼び出して結果をテーブル変数に入れる

DECLARE @Results TABLE (

OrderId INT,

Name NVARCHAR(200),

Amount DECIMAL(18, 2)

);

INSERT INTO @Results

EXEC [RemoteServer].[SalesDB].[dbo].[usp_GetMonthlyOrders]

@Year = 2025,

@Month = 1;

# 取得したデータをローカルのデータと結合

SELECT r.*, c.Email

FROM @Results r

JOIN Customers c ON r.OrderId = c.CustomerId;







> ***注意点***:リモートのストアドを `EXEC` で呼んで結果を `INSERT...EXEC` で受け取る際、***自サーバー側も `BEGIN TRANSACTION` 内にいる場合はMSDTCが必要になる***ことがあります。`BEGIN TRANSACTION` の外側で実行するか、設計を工夫してください。 ---

4部構成名、OPENROWSET、OPENQUERY の説明と特性

#### 📋 4部構成名(Four-Part Name)



# 構造:[リンクサーバー名].[データベース名].[スキーマ名].[テーブル名]

SELECT * FROM [RemoteServer].[SalesDB].[dbo].[Orders];







***特性***

- 最もシンプルに書けるが、最も危険 - SQL Server はクエリ最適化のためにリモートテーブルの統計情報を活用しようとするが、統計情報の収集にリモートアクセスが発生し、かえって遅くなることがある - WHERE句の条件が***ローカルで評価される場合がある***(つまりリモートのフルスキャン後にローカルフィルタ) ***実際のトラブル事例***(実体験):



# リモートの数百万件テーブルから絞り込みしようとした例

SELECT *

FROM [RemoteServer].[SalesDB].[dbo].[Orders]

WHERE OrderDate BETWEEN '2025-01-01' AND '2025-01-31';

# → リモートの Orders 全件(数百万件)がネットワーク転送された後に

# ローカル側で WHERE フィルタが適用

# → ネットワーク帯域の大量消費、処理時間が数十分に

# → タイムアウトやブロックが発生し、他の処理まで影響







#### 📋 OPENROWSET



# 解決策は、OPENROWSET はアドホック接続(リンクサーバー設定不要)

SELECT *

FROM OPENROWSET(

'SQLNCLI17',

'Server=RemoteServer;Database=SalesDB;Trusted_Connection=Yes;',

'SELECT Id, Amount FROM dbo.Orders WHERE OrderDate >= ''2025-01-01'''

);







***特性***

- リンクサーバーを事前設定する必要がない - クエリ文字列を直接渡すため、***リモートサーバー上でそのクエリが実行される***(フィルタがリモートで行われる) - ただし、文字列の中にSQLを書くため可読性が下がりやすい - セキュリティポリシーで `Ad Hoc Distributed Queries` の有効化が必要 > ***⚠️ 文字列内のシングルクォートに注意***:OPENROWSET / OPENQUERY の内側クエリで文字列リテラルを使う場合、シングルクォートを `''` と二重にする必要があります。これはミスが発生しやすいポイントです。


# ❌ NG:シングルクォートが1つだと構文エラー

SELECT * FROM OPENROWSET(..., 'SELECT * FROM Orders WHERE Status = 'New'');

# ✅ OK:シングルクォートを '' と二重にする

SELECT * FROM OPENROWSET(..., 'SELECT * FROM Orders WHERE Status = ''New''');









# Ad Hoc Distributed Queries の有効化(要管理者権限)

EXEC sp_configure 'show advanced options', 1;

RECONFIGURE;

EXEC sp_configure 'Ad Hoc Distributed Queries', 1;

RECONFIGURE;







#### 📋 OPENQUERY(推奨)



# リンクサーバーを使ったOPENQUERY

SELECT *

FROM OPENQUERY([RemoteServer],

'SELECT Id, Amount FROM SalesDB.dbo.Orders WHERE OrderDate >= ''2025-01-01'''

);







***特性***

- クエリがリモートサーバー側で実行されてから結果がローカルに返る(パススルークエリ) - リモートのインデックスが最大限に活用される - 4部構成名と比べて***劇的にパフォーマンスが良い***場合がある ---

4部構成名 vs OPENQUERY:実体験の失敗談から学ぶ

> 「リモートサーバーの tempdb を利用する時、OPENQUERY だと高速で、4部構成名だとブロックの嵐で処理を打ち切るしかなかった」という経験について これは非常に重要な実体験です。***なぜそうなったのか***を解説します:

***4部構成名のケース(地獄の始まり)***



# ❌ NG:リモートの大きな一時テーブルを4部構成名で参照

SELECT *

FROM [RemoteServer].[tempdb].[dbo].[SharedWork]

WHERE SomeColumn = 'TargetValue';

# 内部で起きていること:

# 1. SQL Server がリモートの SharedWork にアクセス

# 2. クエリオプティマイザーがローカルでのフィルタリングを決定

# 3. SharedWork の全件(数百万行)がローカルに転送開始

# 4. ネットワーク・tempdb の両方に対して重いロックがかかり続ける

# 5. 他のプロセスが tempdb やネットワークリソースを使えなくなる

# 6. タイムアウト・ブロックが続出し、最終的に処理を強制終了するしかない







> ***⚠️ さらに重要な注意***:グローバル一時テーブル(`##`)は、作成したセッションが終了すると自動的に削除されます。Linked Server 経由でのアクセスは別セッション扱いになるため、***もとのセッションが生きている間しか参照できません***。また、4部構成名で `tempdb` の `##` テーブルを参照するパターンは動作が不安定なため、実テーブルや OPENQUERY の使用を強く推奨します。 ***OPENQUERY のケース(解決の鍵)***



# ✅ 推奨:同じことをOPENQUERYで行う

SELECT *

FROM OPENQUERY([RemoteServer],

'SELECT * FROM tempdb.dbo.SharedWork WHERE SomeColumn = ''TargetValue'''

);

# 内部で起きていること:

# 1. フィルタリングがリモートサーバー内で実行される

# 2. WHERE 条件に合致した少ない行だけがネットワークを流れる

# 3. tempdb のロック時間は最小限

# 4. 処理が高速に完了する







***まとめると***

比較項目4部構成名OPENQUERY
***WHERE の実行場所***ローカル(危険)リモート(安全)
***ネットワーク転送量***テーブル全件条件一致分のみ
***リモートインデックス活用***不確実確実
***tempdb ロック影響***長時間・広範囲最小限
***使い時***小さなテーブルのみ基本はこちらを使う
---

このように使ってはいけない(サーバーまたがりのNG集)

#### ❌ NG①:トランザクション内でリモートサーバーに大量書き込みをする



# ❌ 分散トランザクション内での大量書き込み

BEGIN TRANSACTION;

INSERT INTO [RemoteServer].[SalesDB].[dbo].[OrderLog]

(OrderDate, CustomerId, Amount)

SELECT OrderDate, CustomerId, Amount

FROM Orders

WHERE Status = 'New'; -- 万件レベル

COMMIT;

# → MS DTC のタイムアウト・ロック嵐・インダウトトランザクションのリスク大







#### ❌ NG②:サーバーまたがりのJOINを複数テーブルスケールで行う



# ❌ ローカルとリモートの複数テーブルを複雑にJOINする

SELECT a.*, b.*, c.*

FROM [RemoteServer1].[DB1].[dbo].[TableA] a

JOIN [RemoteServer2].[DB2].[dbo].[TableB] b ON a.Id = b.AId

JOIN Orders c ON b.Id = c.CustomerId;

# → 最適化が効かず、最悪のプランで動く可能性







***対策***:リモートのデータをローカルの一時テーブルに取得してから JOIN する。



# ✅ 段階的に取得してから JOIN

DROP TABLE IF EXISTS #StageA;

DROP TABLE IF EXISTS #StageB;

CREATE TABLE #StageA (Id INT, Val NVARCHAR(100));

INSERT INTO #StageA

SELECT Id, Val

FROM OPENQUERY([RemoteServer1], 'SELECT Id, Val FROM DB1.dbo.TableA WHERE IsActive = 1');

CREATE TABLE #StageB (AId INT, Val NVARCHAR(100));

INSERT INTO #StageB

SELECT AId, Val

FROM OPENQUERY([RemoteServer2], 'SELECT AId, Val FROM DB2.dbo.TableB WHERE IsActive = 1');

SELECT a.*, b.*, c.*

FROM #StageA a

JOIN #StageB b ON a.Id = b.AId

JOIN Orders c ON b.AId = c.CustomerId;

DROP TABLE #StageA;

DROP TABLE #StageB;







#### ❌ NG③:リンクサーバーの接続タイムアウトを考慮しない

リンクサーバー経由でのクエリには、デフォルトのタイムアウト設定があります。長時間かかるクエリでは途中で切断されることがあります。



# リンクサーバーの接続タイムアウト確認

SELECT name, connect_timeout, query_timeout

FROM sys.servers

WHERE is_linked = 1;







***出力例***



name connect_timeout query_timeout

RemoteServer 0 0







`0` は「タイムアウトなし(無制限)」です。本番環境では適切な値を設定することを検討してください。

#### ❌ NG④:OPENROWSET/OPENQUERY にパスワードを平文で書く



# ❌ パスワードを平文でクエリに書く(ログに残る)

SELECT *

FROM OPENROWSET('SQLNCLI17', 'Server=Remote;Database=DB;User=sa;Password=P@ssw0rd!',

'SELECT * FROM Orders');







パスワードが SQL ログやクエリプランキャッシュに残ります。Windows 認証(Trusted_Connection=Yes)を使うか、資格情報は適切に管理してください。

---

コラム:MSDTCは「素のネットワーク」を愛する

MSDTCとは何か

***MSDTC(Microsoft Distributed Transaction Coordinator)*** は、複数のサーバーにまたがる ***分散トランザクション*** を調整するWindowsのシステムサービスです。サーバーAとサーバーBに対して「どちらの更新も両方コミット、または両方ロールバック」というACIDの原子性を保証します。

前章の「サーバーまたがりのクエリー」において、`BEGIN TRANSACTION` の中からリモートサーバーへの書き込みを行うと、SQL Server は自動的にMSDTCへ分散トランザクションの調整を依頼します。

> ***「なぜ急に出てくるのか」***:リモートへの参照(SELECT)だけならMSDTCは起動しません。リモートへの更新(INSERT / UPDATE / DELETE)、または `BEGIN TRANSACTION` の中でリモートストアドを呼び出した場合にMSDTCが必要になります。 ---

MSDTCの3つの落とし穴

#### 落とし穴①:135番ポートだけでは動かない(RPC動的ポートの呪縛)

MSDTCの通信は2段階になっています。

フェーズ使用ポート役割
***交渉フェーズ***TCP 135(固定)RPCエンドポイントマッパーが「実際に使うポート番号」を返す
***通信フェーズ***TCP 49152~65535(動的)実際のトランザクションデータが流れる
***問題のメカニズム***



クライアント → 135番ポートへ問い合わせ → RPCエンドポイントマッパー

← 「52000番ポートを使ってください」 ←

クライアント → 52000番ポートに直接接続 → ❌ プロキシが塞いでいて到達できない







nginxなどで135番ポートだけをフォワードしても、RPC エンドポイントマッパーが返した動的ポート番号への接続はプロキシを経由しません。クライアントはそのポートに ***直接アクセスしようとする*** ため、プロキシの裏側で動的ポートが遮断されていると、セッションが確立できずに失敗します。

> ***ポートを固定する方法(参考)***:Windowsの「コンポーネントサービス」→「MSDTCのプロパティ」→「セキュリティ」でMSDTCが使用するポートを1つに固定し、そのポートだけをファイアウォールで開けるという方法はあります。ただし、後述の「名前解決の問題」も同時に解決しなければ動きません。 #### 落とし穴②:双方向の名前解決が必須(ホスト名の呪縛)

MSDTCのプロトコル(MS-DTCO)では、***自分のホスト名を通信パケット内に埋め込んで相手に送ります***



サーバーAがサーバーBへ送るパケット内容:

「私はホスト名 SERVER-A です。確認応答は SERVER-A の 52000番ポートへ返してください」







これがnginxなどのプロキシ越しになると、次のことが起きます:



サーバーA(nginx 裏) ──→ nginx ──→ サーバーB

(proxy)

サーバーBが受け取るパケット:「返事は SERVER-A の 52000番ポートへ」

サーバーBの動作:SERVER-A を名前解決して直接接続しようとする

結果:SERVER-A は nginx の裏にいるため到達不能 → トランザクション タイムアウト







プロキシ側のIPやホスト名で通信を中継しても、***DTCプロトコル内に埋め込まれたホスト名は書き換えられません***。そのため、Callback(逆方向接続)のルートが見つからず、トランザクションがタイムアウトします。

> ***補足***:これはHTTPのリバースプロキシとは本質的に異なります。HTTPはアプリケーション層でヘッダを書き換えることができますが、MSDTCのRPCプロトコルはTCPトランスポート上でホスト名を直接埋め込むため、透過的な中継が構造上困難です。 #### 落とし穴③:「設定は正しいのに動かない」という最も気づきにくいパターン

上記2つの問題が組み合わさると、以下のような「どこも間違っていないのに動かない」という状況が発生します:

確認項目状態実際の問題
MSDTCサービスの起動✅ 両サーバーで稼働中問題なし
ファイアウォールの135番✅ 開放済み***動的ポートが未開放***
ホスト名のDNS登録✅ 登録済み***プロキシ越しでは逆引き到達不能***
ネットワーク疎通✅ ping 通る***プロキシを通るため実際のポートに届かない***
> ***実体験より***:「設定は全部正しいはず、それなのになぜ動かないのか」という状況に陥ったとき、まずネットワーク経路にnginxなどのプロキシが介在していないかを確認してください。すべてのMSDTCトラブルの中で最も気づきにくいパターンです。 ---

現実的な判断:「分散トランザクションから降りる」

クラウド、プロキシ、異なるネットワークセグメントなど、複雑なネットワーク構成が前提の企業環境では、***MSDTCに頼る設計自体がリスク***です。次の代替アプローチへの切り替えを早めに検討してください。

アプローチ概要適したシーン
***アプリケーション層でのリトライ処理***更新失敗時にアプリ側でリトライ・補正処理を書く更新頻度が低く、失敗時のリカバリが容易なケース
***結果整合性(Eventual Consistency)の許容***一時的な不整合を許容し、非同期で整合を取るバッチ処理・ログ系テーブルなど即時整合が不要なケース
***更新処理をリモートストアドに委譲***リモートサーバー側のストアドで更新を完結させるローカルからは結果の確認のみ行うケース
***サーバー間の直接接続を確立***プロキシを廃止し、サーバー同士が直接通信できるネットワーク構成にするセキュリティ要件が許す場合の根本解決
> ***現場の知恵として***:「分散トランザクションを諦めて、アプリケーション側での整合性確保に切り替える」という判断は、決して設計の敗北ではありません。***複雑なネットワーク制約の中で安定稼働を優先した、現実的な最適解***です。動かない環境でMSDTCに固執し続けることこそ、最大のリスクです。 ---

まとめ:MSDTCが要求する「3つの素直さ」

要求内容プロキシ環境での問題
***ポートを隠さない***135番に加え、動的ポート(49152~65535)への直接アクセスが必要固定ポートしか通れない経路では動的ポートに到達できない
***名前を隠さない***双方のサーバーがお互いのホスト名で直接到達できる必要があるパケット内のホスト名はプロキシが書き換えられず、Callbackが迷子になる
***経路を曲げない***両サーバー間で対称な通信が成立する必要があるプロキシによる一方向の経路変換はDTCプロトコルと根本的に相性が悪い
セキュリティを強化した企業ネットワークでは、これら3つのうちどれか1つでも欠けるとMSDTCは動作しません。その場合は早期に「アプリケーション側での代替設計」へシフトすることが、現場で生き残るための最適解です。

---

分離レベルと `WITH(NOLOCK)` の罠

分離レベルとは何か

ACID特性の「***I(独立性)***」を具体的にどの程度保証するかを制御するのが ***分離レベル(Isolation Level)*** です。分離レベルを下げると「他のトランザクションの影響を受けやすくなる代わりに、ロックによる待機が減る」というトレードオフがあります。

SQL Server の分離レベルは以下の5段階です(上ほど厳密・遅い、下ほど緩い・速い):

分離レベルダーティリード反復不能読み取りファントムリード特性
***SERIALIZABLE***最も厳密。同一トランザクション内で同じ結果が保証
***REPEATABLE READ***同一行の再読みは保証。新規行の挿入は防げない
***READ COMMITTED***(デフォルト)コミット済みデータのみ読む。SQL Server の標準
***READ UNCOMMITTED***コミット前のデータも読む。最も危険
***SNAPSHOT***行バージョン管理で読取時ロックを回避(tempdbを使用)
> ***用語解説***: > - ***ダーティリード***:他のトランザクションがまだコミットしていない(ROLLBACKされるかもしれない)データを読んでしまう > - ***反復不能読み取り***:同一トランザクション内で同じ行を2回読むと値が変わっている > - ***ファントムリード***:同一トランザクション内で同じ条件で検索すると件数が変わっている ---

`WITH(NOLOCK)` の正体と危険性

現場では「***とりあえずNOLOCKをつける***」という習慣が広がっています。これは非常に危険です。

`WITH(NOLOCK)` は `READ UNCOMMITTED` 分離レベルをそのテーブルだけに適用するヒントです。



# これはやっていることが同じ

SELECT * FROM Orders WITH(NOLOCK);

# ↑ と ↓ は等価

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SELECT * FROM Orders;

SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 戻すのを忘れると大惨事







#### `WITH(NOLOCK)` が引き起こす3つの問題

***問題① ダーティリード:存在しないデータを読む***



# セッション1(ロールバックされる予定のトランザクション)

BEGIN TRANSACTION;

INSERT INTO Orders (OrderDate, CustomerId, Amount, Status)

VALUES ('2025-01-01', 1, 100000.00, 'New');

# まだ COMMIT していない

# セッション2(WITH(NOLOCK) で読んでいる)

SELECT * FROM Orders WITH(NOLOCK) WHERE Amount = 100000.00;

# → ロールバックされるはずのデータが見えてしまう!

# セッション1(ロールバック)

ROLLBACK TRANSACTION;

# → セッション2が読んだデータは「幻のデータ」だった







***問題② 同じ行が2回返ってくる(または行が消える)***

インデックスの再構築中など、ページの物理的な移動が発生している際に `WITH(NOLOCK)` で読むと、同じ行が重複して返ってきたり、本来あるはずの行がスキップされることがあります。これは SQL Server の公式ドキュメントにも記載されている既知の現象です。

***問題③ 隠れた状態管理***

`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED` でセッションレベルに設定した場合、設定を戻し忘れると***そのセッションのすべてのクエリが永続的にダーティリードになります***

---

❌ `WITH(NOLOCK)` のNG集

#### ❌ NG①:「ロック待ちが嫌だから」の乱用



# ❌ 全テーブルにNOLOCKを貼る習慣

SELECT o.*, c.Name

FROM Orders o WITH(NOLOCK)

JOIN Customers c WITH(NOLOCK) ON o.CustomerId = c.CustomerId

WHERE o.Status = 'New';

# → 集計レポートや在庫確認に使うと、架空の数字が混入する







#### ❌ NG②:金額・在庫・ステータスなど「正確性が必要なデータ」にNOLOCKを使う



# ❌ 金額をNOLOCKで確認(コミット前の架空データが混入する恐れ)

SELECT SUM(Amount) FROM Orders WITH(NOLOCK) WHERE CustomerId = 1;

# → 別のトランザクションがロールバックした変更を読んでしまう可能性







---

✅ ロック競合を減らす正しいアプローチ

「NOLOCKをつけたい」という気持ちの背景には「読み取り時のロック待ちをなくしたい」というニーズがあります。これを***安全に解決する方法***があります。

#### ① RCSI(Read Committed Snapshot Isolation)を有効にする

データベースレベルで設定すると、`READ COMMITTED`(デフォルト)の動作が変わり、***読み取りがロックを取得しなくなります***。行バージョン管理(tempdb を使用)を利用します。



# データベースで RCSI を有効にする(要排他的アクセス)

ALTER DATABASE TestDB

SET READ_COMMITTED_SNAPSHOT ON;







RCSI を有効にすると:

- `WITH(NOLOCK)` なしの通常クエリがロックをとらずに読める - ダーティリードは発生しない(コミット済みの最新スナップショットを読む) - ***NOLOCKが不要になる*** > ***代償***:tempdbへの負荷が増えます(前述の「tempdb」セクション参照)。 #### ② 分離レベルを明示的に指定する



# 特定のバッチのみ読み取りロックなしで実行する(使用後は必ず戻す)

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

SELECT * FROM Orders WHERE OrderDate = '2025-01-01';

SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 必ず戻す







***まとめ***

やりたいこと正しい方法危険な方法
読み取りブロックをなくしたいデータベースで RCSI を有効にする`WITH(NOLOCK)` を乱用する
一時的に読み取りロックなしにしたい`SNAPSHOT` 分離レベルを明示指定`READ UNCOMMITTED` をセッションに設定したまま放置
「古くてもいいデータ」を読みたいRCSI または SNAPSHOT`WITH(NOLOCK)`(架空データが混入するリスクあり)
---

よくある質問とトラブルシューティング

Q1. インデックスを作ったのに遅いままです。なぜですか?

***A***. 以下の順番で確認してください:

1. ***実行計画を確認する***:`Index Seek` になっているか、`Table Scan` や `Index Scan` のままか確認する(前述参照) 2. ***SARGable でないWHEREを使っていないか確認***:列に関数を適用したり LIKE '% ...' にしていないか

3. ***インデックスの断片化を確認***:`sys.dm_db_index_physical_stats` で断片化率を確認し、30%超なら `REBUILD`

4. ***統計情報が古い可能性***:`UPDATE STATISTICS テーブル名` を実行して最新化する

5. ***カーディナリティが低い列***:Yes/No フラグのような取り得る値が少ない列はインデックスの効果が薄い

> ***💡 コラム:統計情報とはなにか?なぜ古くなると遅くなるのか?*** > > SQL Server はクエリを実行する前に「***実行計画(Execution Plan)***」を作成します。この計画を作る際に「このテーブルに対象データは何件あるか?」を推定するために使うのが***統計情報(Statistics)***です。 > > 統計情報が古いと、実際には100万件あるテーブルを「1000件のはず」と誤判断し、インデックスを使わない非効率な実行計画を選択することがあります。 > >


> -- 統計情報の最終更新日を確認する > SELECT > OBJECT_NAME(s.object_id) AS table_name, > s.name AS stats_name, > sp.last_updated, > sp.rows, > sp.rows_sampled, > sp.modification_counter -- この値が大きいほど古い > FROM sys.stats s > CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp > WHERE OBJECT_NAME(s.object_id) = 'Orders' > ORDER BY sp.modification_counter DESC; >






> > `modification_counter` が大きい(データの変更回数が多い)のに `last_updated` が古い場合は、手動更新を検討してください。SQL Server はデフォルトで自動更新しますが、大量データのテーブルでは自動更新の閾値(20%変更)に達しにくく、古い状態が続くことがあります。

Q2. デッドロックが頻発しています。どうすればよいですか?

***A***. デッドロックトレースを取得して根本原因を特定してください。

> ***⚠️ 注意***:デッドロックは SQL Server が即座に解消する(片方をロールバックする)ため、`sys.dm_exec_requests` では捕捉できません。過去に発生したデッドロックを調べるには、***system_health 拡張イベント*** を使います。


# system_health 拡張イベントからデッドロックのXMLを取得する

SELECT

xdr.value('@timestamp', 'datetime2') AS deadlock_time,

xdr.query('.') AS deadlock_graph

FROM (

SELECT CAST(target_data AS XML) AS target_data

FROM sys.dm_xe_session_targets t

JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address

WHERE s.name = 'system_health'

AND t.target_name = 'ring_buffer'

) AS data

CROSS APPLY target_data.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS xdr_tab(xdr)

ORDER BY deadlock_time DESC;







上記クエリで取得した XML を SSMS の「デッドロックグラフ」で視覚化すると、どのセッションがどのリソースを巡って競合したかが一目で分かります。

***現在進行中のブロッキングを確認する場合***(デッドロックではなく、待機中のセッションを見る):



# ブロッキングセッションの確認(リアルタイム)

SELECT

r.session_id,

r.blocking_session_id,

r.wait_type,

r.wait_time,

t.text AS sql_text

FROM sys.dm_exec_requests r

CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t

WHERE r.blocking_session_id > 0;







デッドロックは Extended Events(推奨)で継続的にキャプチャするか、SSMS の「拡張イベント」→「system_health」セッションから過去の履歴を確認できます。根本原因は多くの場合「テーブルアクセス順序の不一致」なので、***全処理で同じ順序でテーブルに触る設計に変更する***ことが最も効果的です。

Q3. tempdb が肥大化して、ディスクがいっぱいになりました。

***A***. 原因を特定してから対処してください:



# tempdb を大量使用しているセッションを特定

SELECT

t.session_id,

t.internal_objects_alloc_page_count * 8 / 1024 AS internal_MB,

t.user_objects_alloc_page_count * 8 / 1024 AS user_MB,

r.blocking_session_id,

s.text AS sql_text

FROM sys.dm_db_task_space_usage t

JOIN sys.dm_exec_requests r ON t.session_id = r.session_id

CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) s

WHERE t.internal_objects_alloc_page_count + t.user_objects_alloc_page_count > 0

ORDER BY (t.internal_objects_alloc_page_count + t.user_objects_alloc_page_count) DESC;







***出力例***



session_id internal_MB user_MB sql_text

58 4096 0 SELECT ... ORDER BY 30列(ソート操作で爆発)







この例では、セッション58が 4GB のソート作業領域を tempdb に展開しています。クエリを改善(インデックス追加やORDER BYの見直し)して対処します。

Q4. サーバーまたがりのクエリがタイムアウトします。

***A***. 以下のチェックリストを確認してください:

1. ***OPENQUERY を使っているか***:4部構成名から OPENQUERY に切り替える(前述参照) 2. ***リモートのインデックスが適切か***:リモートサーバーに実際にログインして実行計画を確認する

3. ***リンクサーバーの設定***:`query_timeout` の値を確認・調整する

4. ***ネットワーク帯域の確認***:転送量が多い場合、ネットワーク自体がボトルネックの可能性

5. ***MS DTC の動作確認***:分散トランザクションを使っている場合、MS DTC サービスが両サーバーで動作しているか確認



# リンクサーバーの接続テスト

EXEC sp_testlinkedserver [RemoteServer];







***成功時の出力***



EXEC sp_testlinkedserver [RemoteServer] が正常に完了しました。







Q5. `##グローバル一時テーブル` は削除されたはずなのに「既に存在する」エラーが出ます。

***A***. グローバル一時テーブルは「全セッションがアクセスを終えたとき」に削除されます。他のセッションがまだ参照中の場合は削除されません。

確認方法:



# ##テーブルを参照しているセッションを調査

SELECT

o.name,

r.session_id,

r.status,

t.text

FROM tempdb.sys.objects o

JOIN sys.dm_exec_requests r ON r.session_id != @@SPID

CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t

WHERE o.name LIKE '##%'

ORDER BY o.name;







Q6. GROUP BY を使ったクエリで ORDER BY がエラーになります。

***A***. `GROUP BY` を使ったクエリでは、`ORDER BY` に指定できる列は ***SELECT句に含まれているものか、集計関数の結果のみ***です。



# ❌ NG:SELECT句にない OrderDate で ORDER BY しようとするとエラー

SELECT CustomerId, SUM(Amount) AS TotalAmount

FROM Orders

GROUP BY CustomerId

ORDER BY OrderDate; -- エラー: ORDER BY 句の列 'OrderDate' が無効です

# ✅ OK:SELECT句に含まれる列または集計結果で並べ替える

SELECT CustomerId, SUM(Amount) AS TotalAmount

FROM Orders

GROUP BY CustomerId

ORDER BY TotalAmount DESC; -- 集計結果で並べ替え

# または

ORDER BY CustomerId; -- GROUP BY 列で並べ替え







---

SQL Server 2025 と AI の未来



2026年現在、SQL Server にはさらに驚くべき進化が訪れています。それが***SQL Server 2025*** です!!

このバージョンでの最大の目玉は、なんといっても***Vector Support(ベクトルデータ型のネイティブサポート)***!!

これにより、テキストや画像をベクトル化して保存し、SQL文で直接「意味による検索(ベクトル検索)」が可能になりました。最近流行りの***RAG(検索拡張生成)***をシステムに組み込む際、わざわざ別のベクトルデータベースを用意することなく、長年使い慣れた SQL Server だけで完結できるようになったんです!!

まさに、伝統あるデータベースが最新のAI技術と手を取り合った瞬間ですね!!

---

SQL Server チューニングの現実と未来

「これはゲーム開発並みの複雑さではないか」という問いに向き合う

本ガイドブックを通じて感じていただけたかもしれません——SQL Server を「まともに動かす」ためにやらなければならないことの多さを。インデックス設計、断片化メンテ、統計情報の鮮度管理、tempdb のファイル分割、NOLOCK を避けるためのRCSI設定、分散トランザクションの罠……。

これはゲーム開発で言えば、***描画最適化・メモリ管理・ネットワーク遅延補正・物理演算の精度調整をすべて手動でやれ***と言われているようなものです。熟練者でなければ安定稼働すら難しい。

> ***正直に言えば、これは「設計が悪い」のではありません。SQL Server がこれほどの複雑さを持つのは、30年以上の進化の中で膨大な要求仕様を詰め込み続けた結果です。しかし「それが理由だから仕方ない」と言い続けることが、この技術の未来にとって良いことかどうかは別の話です。*** ---

若手開発者が遠ざかるリスク

これは業界全体の問題として、静かに進行しています。

問題現状
***学習コストの高さ***インデックス・ロック・tempdb・分散トランザクションを理解するのに数年かかる
***失敗が見えにくい***「動いているけど遅い」「たまにデッドロックする」はすぐにはわからない
***エラーメッセージが不親切***「オブジェクト名 'X' は無効です」ではなぜ無効かまで教えてくれない
***ドキュメントの深さ***公式ドキュメントが豊富な反面、初学者が何から読めばいいかわからない
***「聞ける人」問題***SQL が得意なベテランは数少なく、職場で質問できる環境が整っていない
若手がリレーショナルデータベースよりも MongoDB・Firebase・DynamoDB などのNoSQLや、SQLを意識させない ORM に流れていく理由は明確です。***「とりあえず動かせる」までの距離が圧倒的に短い***からです。

この流れを「嘆かわしい」と言うのは簡単ですが、現場の開発者として正直なところ、***彼らの選択は合理的です***。複雑なものを複雑なまま維持し続けることを、次の世代に強いるのは持続可能ではありません。

---

Microsoft と業界は何をしているか

明るい話もあります。Microsoft はこの問題を認識しており、「人間がやらなくていいことをシステムが自動化する」方向に継続的に投資しています。

#### ① Intelligent Query Processing(IQP)— SQL Server 2019 以降

従来は「人間が実行計画を見て、インデックスを張り、クエリを書き直す」という手作業だったチューニングの一部を、SQL Server のエンジン自体が自動調整するようになっています。

機能説明
***アダプティブ結合***実行中にデータ量を見て、Hash Join か Nested Loop か自動選択
***行数推定フィードバック***実行後に統計情報のズレを学習し、次回の実行計画を自動修正
***メモリ許可フィードバック***ソート・ハッシュ操作のメモリ不足・過剰を自動調整
***近似COUNT DISTINCT***大量データの集計クエリを精度を保ちながら大幅高速化


# IQP の有効化はデータベースの互換性レベルを 150(SQL Server 2019)以上にするだけ

ALTER DATABASE TestDB SET COMPATIBILITY_LEVEL = 160; -- SQL Server 2022







#### ② Automatic Tuning(自動チューニング)— SQL Server 2017 以降 / Azure SQL

手動での実行計画管理を自動化する機能です。



# 自動チューニングの状態確認

SELECT name, desired_state_desc, actual_state_desc, reason_desc

FROM sys.database_automatic_tuning_options;

# 自動インデックス作成・削除を有効化(Azure SQL Database で特に有効)

ALTER DATABASE TestDB

SET AUTOMATIC_TUNING (CREATE_INDEX = ON, DROP_INDEX = ON);







> ***注意***:オンプレミスの SQL Server では `FORCE_LAST_GOOD_PLAN`(退行した実行計画を自動で戻す機能)のみがデフォルト有効です。`CREATE_INDEX` の自動適用は主に Azure SQL Database 向けです。 #### ③ Query Store — SQL Server 2016 以降

「なぜ急に遅くなったのか」を時系列で追跡できる機能です。実行計画の履歴を自動保存し、パラメータスニッフィングによる退行を視覚化・強制修正できます。



# Query Store の有効化

ALTER DATABASE TestDB

SET QUERY_STORE = ON

(

OPERATION_MODE = READ_WRITE,

MAX_STORAGE_SIZE_MB = 500,

QUERY_CAPTURE_MODE = AUTO

);

# 遅いクエリのトップ10を確認

SELECT TOP 10

qt.query_sql_text,

qrs.avg_duration / 1000.0 AS avg_ms,

qrs.count_executions

FROM sys.query_store_query_text qt

JOIN sys.query_store_query q ON qt.query_text_id = q.query_text_id

JOIN sys.query_store_plan qp ON q.query_id = qp.query_id

JOIN sys.query_store_runtime_stats qrs ON qp.plan_id = qrs.plan_id

ORDER BY qrs.avg_duration DESC;







#### ④ Azure SQL Database / Managed Instance:クラウドが吸収してくれること

オンプレミスの SQL Server で人間が手動でやっていることの多くは、Azure SQL に移行することで消えます。

オンプレミスで人間がやることAzure SQL での扱い
tempdb のファイル分割自動最適化済み
インデックス断片化メンテ自動メンテナンスウィンドウ
統計情報の更新自動更新(チューニング済み)
バックアップ管理全自動
パッチ適用自動(ダウンタイム最小化)
スケールアップ/ダウンボタン1つまたは自動スケーリング
***「その複雑さをクラウドに押し込めて、開発者は本来の業務に集中できる」***というのが Microsoft の現在の回答です。

---

現場の開発者へ:この複雑さとどう向き合うか

> ***個人的な見解として***:SQL Server のチューニング知識は、まるで「車のエンジンをキャブレターレベルで理解していないと運転できない」ような状態です。現代の車はECUが自動制御してくれるように、データベースもその方向に進んでいます。 ***若手開発者の方へ***:今のSQL Serverの複雑さは、あなたが「向いていない」からわからないのではありません。それは本当に難しいのです。Query Storeと実行計画の読み方、インデックスのSARGable原則、この2つを押さえるだけでも現場の大半の問題は解決できます。全部を一度に覚える必要はありません。

***ベテランの方へ***:自分が長年かけて身につけた知識が若手に伝わらないとき、それは「今の若者はやる気がない」のではなく、「学習コストが高すぎる構造になっている」可能性を考えてみてください。ドキュメントに「なぜこうなるのか」を書く文化、失敗を責めない現場の雰囲気、それが最も効果的な「育成チューニング」です。

***システム設計者の方へ***:新規プロジェクトではSQL Server の採用理由を明確にしてください。大量の同時書き込みが要件にない場合、PostgreSQL・MySQL・Azure SQL との比較検討を行うことが現代の設計責任です。SQL Server が正解のケースは確実に存在しますが、「昔からそうだったから」という理由での選択は技術的負債になります。

---

まとめ:SQL Server の現実と期待

観点現実期待される変化
***チューニング複雑度***ゲーム開発並みの専門性が必要IQP・自動チューニングで人間の介入を減らす方向
***若手の参入障壁***高い。NoSQLやORMに流出クラウド移行でインフラ運用を隠蔽することで緩和可能
***マイクロソフトの姿勢***Azure SQL・IQP・自動チューニングに積極投資AIとの統合(SQL Server 2025のベクター型・自然言語クエリ)で加速
***オンプレミスの未来***セキュリティ・コンプライアンス要件があれば継続クラウドへの段階移行が現実的な選択肢
SQL Server は複雑です。しかしその複雑さは「難しくしてやろう」という意図ではなく、***エンタープライズの現実に真剣に向き合ってきた証拠でもあります***。複雑さを理解したうえで「どこを自動化し、どこを人間が管理するか」を選択できることが、現代のデータベースエンジニアに求められる素養です。

---

まとめ

📖 本ガイドブックでは、SQL Server の以下のテーマについて「なぜそうなるのか」と「やってはいけない」を軸に解説しました:

テーマ最重要ポイント
***インデックス***SARGable なクエリを書く。インデックスは多すぎてもNG。断片化を定期メンテする
***トランザクション***短く・TRY-CATCH で囲む。分散トランザクションは慎重に
***tempdb***全ユーザー共有の有限リソース。使い終わったら即DROP。複数ファイル化推奨
***#・##テーブル***ローカル一時テーブルを積極利用。##はビジネスロジックに使わない
***サーバーまたがり***4部構成名よりOPENQUERY。リモートにストアドを置いて結果を引き込む
***MSDTC***プロキシ・クラウド環境では動的ポートとホスト名の問題で動作しないケースが多い。代替設計への早期シフトが現実解
***分離レベル・NOLOCK***`WITH(NOLOCK)` は「速いが嘘をつく」。正しい対策は RCSI の有効化
***チューニングの現実***複雑さを認識しつつ、IQP・自動チューニング・クラウドで負荷を減らす方向へ

SQL Server チューニングのゲーム開発的思想

- 🎮 ***ロード時間の短縮は「不要なデータの読み込みをやめること」から始まる*** → インデックスでフルスキャンを Index Seek に変える=必要な箇所だけ読む

- 🎮 ***ゲームのラグはネットワーク往復にある*** → サーバーまたがりではリモートで絞り込み、少ないデータだけ転送する

- 🎮 ***シェアードリソースは「全員が影響を受ける」前提で設計する*** → tempdb は全ユーザー共有。自分のクエリが他人のパフォーマンスを殺していないか常に意識する

- 🎮 ***プロゲーマーだけが楽しめるゲームは、普及しない*** → チューニングの複雑さを次世代に丸投げせず、自動化・標準化・ドキュメント化で「誰でも参加できる現場」を作る

---

参考リンク一覧

リンク詳細
[SQL Server 公式ドキュメント(インデックスの設計ガイド)](https://learn.microsoft.com/ja-jp/sql/relational-databases/sql-server-index-design-guide)Microsoft 公式のインデックス設計ガイド
[SQL Server トランザクション ガイド](https://learn.microsoft.com/ja-jp/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide)ロック・行バージョン管理の詳細
[トランザクション分離レベル](https://learn.microsoft.com/ja-jp/sql/t-sql/statements/set-transaction-isolation-level-transact-sql)分離レベルの詳細リファレンス
[Read Committed スナップショット分離(RCSI)](https://learn.microsoft.com/ja-jp/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide?view=sql-server-ver16#read_committed_snapshot_isolation_level)RCSI の設定と動作の詳細
[tempdb データベース](https://learn.microsoft.com/ja-jp/sql/relational-databases/databases/tempdb-database)tempdb の最適化ガイド
[リンク サーバー(SQL Server)](https://learn.microsoft.com/ja-jp/sql/relational-databases/linked-servers/linked-servers-database-engine)リンクサーバーと分散クエリの詳細
[OPENQUERY (Transact-SQL)](https://learn.microsoft.com/ja-jp/sql/t-sql/functions/openquery-transact-sql)OPENQUERY 関数リファレンス
[分散トランザクション コーディネーター(MSDTC)の構成](https://learn.microsoft.com/ja-jp/troubleshoot/windows-server/application-management/configure-dtc-to-work-through-firewall)ファイアウォール越しのMSDTC設定ガイド
[Intelligent Query Processing(IQP)](https://learn.microsoft.com/ja-jp/sql/relational-databases/performance/intelligent-query-processing)SQL Server 2019以降の自動最適化機能
[自動チューニング](https://learn.microsoft.com/ja-jp/sql/relational-databases/automatic-tuning/automatic-tuning)自動チューニングの概要と設定方法
[Query Store の使用](https://learn.microsoft.com/ja-jp/sql/relational-databases/performance/monitoring-performance-by-using-the-query-store)クエリパフォーマンス履歴の管理
--- ***更新日時***:2026 年 04 月 16 日

***本ガイドブックは、SQL Serverをすでに使っている経験者の方が「あやふやをなくし」「やってはいけないことを知る」ことを目的として作成されています。内容に関するご質問や追加テーマのご要望がございましたら、いつでもお気軽にお声がけください。***

--- **更新日時**:2026 年 04 月 16 日

***本ガイドブックは、初心者から中級者まで幅広にご利用いただけるよう、実践的かつ技術的な内容をバランスよく盛り込んでいます。ご不明な点やご質問がございましたら、いつでもお気軽にお声がけください。***


それではまた次の記事でお会いしましょう!!