SQL Server の行サイズの見積もりと現状診断
拡張項目(分類・数値・日付・説明・チェック・添付ファイル)を増やすと、SQL Server の 1 行あたりのサイズが増えます。SQL Server の通常のテーブル(ディスクベース・非圧縮)は、行内に置けるデータが 1 行 8,060 バイトまでです。このページでは、その余裕を見積もる方法と、実際の DB の列定義・保存データ量・行外領域の使い方を読み取りだけで確かめる方法をまとめます。
- 行サイズは 固定長データ・可変長データの行内部分・管理領域 に分けて評価します。
- 項目数だけでは、可変長データや行外参照を含めた「安全に増やせる数」は決まりません。手入力の概算と実 DB の診断を使い分け、最大入力時と各操作を検証環境で確かめます。
- ここで紹介する 2 つの PowerShell スクリプト(ページ内に全文を掲載)は、どちらも DB を変更しません。現状診断のスクリプトは実機の SQL Server での接続確認をしていないので、まず検証環境で試してください。
プリザンターの拡張項目の列の型
拡張項目の列の型は App_Data/Parameters/ExtendedColumnDefinitions/*.json で決まっています(1.5.8.1)。
| 項目 | 列名 | SQL Server の型 | 行内の扱い |
|---|---|---|---|
| 分類 | ClassA 〜 | nvarchar(1024) | 可変長。宣言上の最大は 1 列 2,048 バイト |
| 数値 | NumA 〜 | decimal(19,4) | 固定長。1 列 9 バイト |
| 日付 | DateA 〜 | datetime | 固定長。1 列 8 バイト |
| 説明 | DescriptionA 〜 | nvarchar(max) | 可変長(max) |
| チェック | CheckA 〜 | bit | 固定長。bit 列 8 列ごとに 1 バイト |
| 添付ファイル | AttachmentsA 〜 | nvarchar(max) | 可変長(max)。ファイル本体ではなく JSON を保存する |
根拠: Num.json、Class.json、Date.json、Description.json、Check.json、Attachments.json。添付ファイルの列に JSON を入れるのは RecordingJson() です(Def.cs#L6680-L6690)。
- 標準では Depts・Groups・Users・Issues・Results の 5 テーブルに、6 種類 × A〜Z の 26 列ずつが作られます(Def.cs#L6494-L6548)。
- 商用ライセンスでは
App_Data/Parameters/ExtendedColumns/*.json(ExtendedColumnsSet)でClass001のような列を種類ごとの数だけ足せ、DisabledColumnsで標準の列を外せます(Def.cs#L2583-L2587、Def.cs#L6550-L6582、ExtendedColumns.cs)。 - 列はサイトごとではなく、全サイトが共有する
Results・Issuesなどの物理テーブルに作られます。_history・_deletedにも同じ列ができるので(データベースのテーブル構成)、それぞれを個別に評価します。 - 画面で項目を非表示にしても物理列はなくなりません。数値の小数点以下の表示桁数を変えても、
decimal(19,4)の格納サイズは変わりません。
行サイズの簡易計算
PowerShell 7 以降で、項目数と想定データ量から 1 行の固定長・可変長・管理領域と、8,060 バイトまでの余裕 を計算するスクリプトです(全文は Measure-SqlServerRowSize.ps1)。DB には接続しません。通常のディスクベース・非圧縮テーブルを対象にした 可変長データをすべて行内に置いた場合の概算 で、行外に移したあとのサイズや保存できるかどうかを判定するものではありません。
./Measure-SqlServerRowSize.ps1 `
-NumCount 10 -NumPrecision 19 -DateCount 10 -CheckCount 8 `
-ClassCount 20 -ClassBytesPerColumn 40 `
-DescriptionCount 2 -DescriptionBytesPerColumn 200 `
-AttachmentCount 1 -AttachmentBytesPerColumn 300この例は固定長 171、可変長 1,500、管理領域 61、合計 1,732 バイト、余裕 6,328 バイト です。説明用の項目構成で、プリザンターの標準列(ID・タイトル・本文・コメント・更新日時など)のサイズは含んでいません。
| 引数 | 指定内容 |
|---|---|
NumCount / NumPrecision | 数値列の数と decimal の精度。スクリプトの既定値は 18 だが、プリザンターの数値列は decimal(19,4) なので 19 を指定する(18 でも 19 でも 1 列 9 バイトで結果は同じ) |
DateCount | datetime 列の数(1 列 8 バイト) |
CheckCount | 標準列を含む全 bit 列の数。8 列ごとに 1 バイト |
ClassCount / ClassBytesPerColumn | 分類列の数と、1 列あたりの保存文字列の想定バイト数 |
DescriptionCount / DescriptionBytesPerColumn | 説明列の数と、1 列あたりの保存文字列の想定バイト数 |
AttachmentCount / AttachmentBytesPerColumn | 添付ファイル列の数と、1 列あたりの保存 JSON の想定バイト数。ファイル本体の容量ではない |
OtherFixedCount / OtherFixedBytes | 上記に含めなかった固定長列の数と、そのデータサイズの 合計 |
OtherVariableCount / OtherVariableBytes | 上記に含めなかった可変長列の数と、そのデータサイズの 合計 |
- 列数の既定値は 0 です。文字列・その他の列数を指定したら、対応するバイト数も明示します(空なら 0 を指定。省略するとエラー)。
- NULL や空文字列ならデータ部分は 0 ですが、列数には含めます。固定長列は NULL でも領域を使います。
- bit 列は
OtherFixedCountに混ぜずCheckCountにまとめます。精度の違う decimal や datetime2 などはOtherFixedCount/OtherFixedBytesで計上できます。 - 列数・データ量を二重に数えないこと、管理領域を入力値に足さないことに注意します。
計算式
EstimatedInlineBytes = FixedBytes + ManagementBytes + VariablePayloadBytes
- 固定長: decimal は精度 1〜9 が 5、10〜19 が 9、20〜28 が 13、29〜38 が 17 バイト/列。datetime は 8 バイト/列。bit は
ceil(bit 列数 / 8)バイト。 - 管理領域: 行ヘッダー 4 + 列数情報 2 + NULL ビットマップ
ceil(全列数 / 8)。可変長列があれば、さらに列数情報 2 + オフセット配列2 × 可変長列数。 RemainingBytes: 8,060 から概算を引いた値。負なら、行内に置く仮定では超えています。InlineScenarioFits: その仮定だけで 8,060 以内かどうか。Trueでも実運用で安全とは限りません。
末尾の NULL による管理領域の省略は見込まず、全列を数えます。圧縮、SPARSE 列、行バージョン情報などの追加メタデータや特殊な行形式は扱いません。8,060 バイトぎりぎりの設計は避けてください。
分類・説明・添付ファイルの見積もり方
| 対象 | 行内/行外の考え方 |
|---|---|
分類の nvarchar(1024) | 最大の 2,048 バイトを常に使うわけではないので、実際の保存文字列で数える。ROW_OVERFLOW に退避した列は、行内に 24 バイトの参照を残す(オフセットは別) |
説明・添付の nvarchar(max) | 小さい値は行内に残ることがあり、大きい値などは LOB_DATA に退避する。行内に残る LOB 参照の構造は格納方式によるので、一律 24 バイトとはしない |
| 添付ファイル本体 | Binaries テーブルや設定した保存先で別に評価する(添付ファイルの保存先)。レコードの行では、識別子・ファイル名などを含む JSON の文字列を数える |
- 通常の日本語は概ね 1 文字 2 バイトですが、絵文字などは 4 バイトになることがあります。
- 入力値には
LENではなくDATALENGTHで調べたバイト数を使います。分類の複数選択は、表示ラベルではなく保存値で評価します。 DATALENGTHは行外のデータも含むデータ全体の長さで、実際の行内の占有量ではありません。- 列ごとに長さが違うときは、各グループの最大値を 1 列あたりの値にすると保守的な見積もりになります。
- NULL・空文字列・
[]は区別します。[]は nvarchar で 4 バイトの非 NULL データです。
スクリプトは ROW_OVERFLOW への退避を模擬しません。たとえば分類 100 列をすべて行外に出せたとしても、参照 2,400 + オフセット 200 + NULL ビットマップの増加 12〜13 バイト程度が行内に残ります(元のテーブルに可変長列がなければ列数情報 2 バイトも必要)。固定長の数値・日付列は、通常の ROW_OVERFLOW では退避できません。
また、非 NULL の max 列には ソート時に 1 列 24 バイトの追加の固定領域 が要ることがあり、保存される行の LOB 参照とは別物です。この作業領域もスクリプトの計算には入っていません。
使うときの確認
sys.columns/sys.typesで実際の DDL を確かめ、ID・タイトル・本文・コメント・更新日時などの標準列も含めて入力します。Results・Issuesと、対応する_history・_deletedなどを個別に評価します。- 通常時と最大入力時の両方で計算します。説明の入力上限、分類の最大選択数、添付件数・長いファイル名を考えます。
- 本番相当の検証環境で、登録・更新・履歴作成・削除・復元・一覧のソート・エクスポートを確かめます。CodeDefiner で列の追加に成功しても、その後のデータ登録や操作が成功するとは限りません。
足りないときは、拡張列の数や入力上限を減らす、関連するテーブルに分けるといった方法を検討します。型の変更や行外格納の設定だけに頼らず、プリザンター/CodeDefiner との整合も確かめてください。
参考(SQL Server の仕様): ROW_OVERFLOW(大きな行のサポート)、nvarchar と max 列の注意事項、decimal の格納サイズ
実 DB の現状診断
手入力の代わりに、PowerShell 7 以降で SQL Server 2016 以降の 実 DB の列定義・保存データ量・行外領域の使用状況 を確かめるスクリプトです(全文は Get-SqlServerRowDiagnostics.ps1)。問い合わせは読み取りだけで、テーブルや設定は変えません。業務データの本文は出力しません。
$report = ./Get-SqlServerRowDiagnostics.ps1 `
-Server "sqlserver.example.local" -Database "Pleasanter" `
-Schema "dbo" -Table "Results" -IncludeRelated
$report | ConvertTo-Json -Depth 8SQL Server 認証を使うときは、パスワードをコマンドに直書きせず対話で入力します。
$credential = Get-Credential
$report = ./Get-SqlServerRowDiagnostics.ps1 `
-Server "sqlserver.example.local" -Database "Pleasanter" `
-Table "Issues" -Credential $credential
$report | ConvertTo-Json -Depth 8| 引数 | 内容 |
|---|---|
Server / Database / Table | 必須。接続先、DB 名、物理テーブル名。Table にはスキーマを含めない |
Schema | スキーマ名。既定値 dbo |
Credential | SQL Server 認証用の PSCredential。省略すると統合認証。Windows 以外では統合認証の事前設定が必要 |
IncludeRelated | 指定したテーブルと同じスキーマの _history・_deleted も個別に診断する |
SampleRows | 軽量確認で読む行数の上限。既定値 1,000、1〜100,000 |
Detailed | データの全行集計と、物理レコードサイズの統計(詳細)を取る |
CommandTimeout | 各問い合わせのタイムアウト秒数。既定値 30、1〜600 |
接続は暗号化し、サーバー証明書を検証します(Encrypt=true、TrustServerCertificate=false)。サーバー名と証明書が一致し、実行する端末で証明書チェーンを信頼できる状態にしてください。検証を無効にするオプションはありません。接続タイムアウトは 15 秒です。.NET の System.Data.SqlClient を使い、追加の PowerShell モジュールは要りません。
軽量確認と詳細確認
| 内容 | 通常の実行(軽量) | -Detailed |
|---|---|---|
| 列定義・固定長/管理領域の概算 | 取得 | 取得 |
| 行数の概数、IN_ROW/ROW_OVERFLOW/LOB の使用ページ | DMV から取得 | DMV から取得 |
可変長データの DATALENGTH 集計 | 最大 SampleRows 行 | 全行 |
| 物理レコードサイズの統計 | 取得しない | sys.dm_db_index_physical_stats の DETAILED で取得 |
軽量確認はランダムサンプルではなく、詳細確認は重い
通常の実行は TOP で並び順を指定せずに読むだけで、ランダムサンプルではありません。サンプルの外にある長いデータを見落とすので、サンプルの最大値をテーブル全体の最大値と見なさないでください。行数の上限は読むバイト数や I/O の上限ではないので、大きい LOB を持つ行では軽量確認でも負荷がかかります。
詳細確認は全行・物理ページを走査します。まず検証環境で実行し、本番では負荷の低い時間帯に限ってください。読み取りでもロック待ちや、可用性グループのセカンダリで REDO を妨げることがあります。
結果の読み方
テーブルごとに 1 つのオブジェクトを返します。各セクションは Status・Reason・Data を持ち、Columns だけは列定義の配列です。
| 出力 | 確かめる内容 |
|---|---|
Metadata / Columns | テーブルの格納形式、各列の実際の型・長さ・精度・NULL 可否など |
Budget | 列定義から求めた固定長・管理領域。可変長列を宣言上の最大長まで行内に置く仮定の概算 |
Allocation | パーティション別と合計の行数の概数、行内・ROW_OVERFLOW・LOB の使用ページ数 |
Payload | 対象の行数、行ごとの可変長データ合計の平均・最大、列ごとの非 NULL 件数・平均・最大 |
PhysicalStats | 詳細確認のときの、パーティション別の物理レコード件数と最小・平均・最大サイズ |
Budgetは実際の型・精度と格納される列から、固定長部分と管理領域を計算します。通常の行形式で計算できない型や、圧縮・列ストア・メモリ最適化テーブル・非一意のクラスター化インデックスなどがあれば、概算を出さず理由を示します。nvarchar(max)などがあっても、通常の行形式なら固定長・管理領域は表示します。ただし可変長全体の宣言上の最大長が決まらないので、Budget.StatusはPartialになり、合計サイズ・残りバイト数・収まるかどうかは未確定(NULL)です。プリザンターのResults・Issuesは本文・コメント・説明・添付ファイルがnvarchar(max)なので、Partialになります。固定長と管理領域だけの値には、可変長データや行外参照は含まれません。- 行外領域はヒープ/クラスター化インデックスが対象で、非クラスター化インデックスの分は足しません。使用ページが正ならその領域を使っていることは分かりますが、どの列・行が行外にあるかや正確な件数を示す値ではありません。
DATALENGTHは行外の部分も含む保存データ量で、行内の占有量ではありません。添付ファイル列の保存 JSON は対象ですが、別テーブルやファイルストレージの添付本体は含みません。対象は格納される可変長の文字列・バイナリ列で、行の合計では NULL を 0、列ごとの平均では NULL を除きます。行の最大値は同じ行の合計の最大で、各列の最大値を足した値とは別です。- 対象の列に暗号化列や動的データマスキング列があると、誤ったサイズを出さないよう
Payloadを取得不可にします。テーブルにマスキング列があり、対象に永続化された計算列がある場合も、マスキングの継承を考えて取得不可にします。マスキング列はUNMASK権限の有無にかかわらず対象外です。 - 詳細確認の物理統計はパーティション別に見ます。保存済みのレコードの大きさであって、将来の入力やソート時の作業領域を保証するものではありません。
- 権限不足・タイムアウトなどは「取得不可」として扱い、0 バイトや行外格納なしとは判定しません。指定したテーブルが見えないとエラー、
_history・_deletedが見えないと警告になります。 - 空のテーブルでは、データの最大値・平均値は未定義(NULL)です。
必要な権限と制約
対象テーブルの SELECT と列定義の参照権限が必要です。DMV には別に参照権限が要り、sys.dm_db_partition_stats は SQL Server 2019 以前では VIEW DATABASE STATE と VIEW DEFINITION、SQL Server 2022 以降では VIEW DATABASE PERFORMANCE STATE と VIEW SECURITY DEFINITION を求めます。物理統計に必要な権限は SQL Server のバージョン・対象範囲で違います。DB 管理者と必要最小限の権限を確かめ、管理者権限や書き込み権限を安易に与えないでください。
- 問い合わせは一括のスナップショットではないので、更新中の DB では結果ごとに取得時点が違います。
- 保存データの集計の対象は、実行ユーザーに見える行です。行レベルセキュリティで除かれた行は詳細確認でも含まれず、パーティション全体の行数・物理統計とは範囲が違うことがあります。
- 列を削除したあとの物理領域や行バージョン情報などは、列定義からの概算には反映されません。
「現状を確かめる」診断であり、「あと何項目まで安全に追加できるか」を保証するものではありません。 項目を増やす前に、最大入力時と、登録・更新・履歴・削除・復元・一覧のソートなどの動作も検証してください。
参考(SQL Server の仕様): sys.dm_db_partition_stats(使用状況と権限)、sys.dm_db_index_physical_stats(物理統計と負荷・権限)
スクリプト全文
Measure-SqlServerRowSize.ps1
Measure-SqlServerRowSize.ps1 をダウンロード
Measure-SqlServerRowSize.ps1(行サイズの簡易計算)
#Requires -Version 7.0
<#
.SYNOPSIS
Estimates an uncompressed SQL Server row before off-row storage.
.DESCRIPTION
Supply physical column counts, including standard columns, for one table.
Num columns use decimal(NumPrecision, s); Date columns use datetime (8 bytes).
CheckCount includes ALL bit columns, packed together.
String byte counts are per column (DATALENGTH), not character counts or file sizes.
When a string count is positive, its byte estimate must be supplied explicitly.
OtherFixedBytes and OtherVariableBytes are totals, excluding row overhead.
NULL fixed columns still consume space. NULL strings have zero payload.
This assumes all variable payloads stay in-row. It does not predict ROW_OVERFLOW,
LOB references, compression, versioning metadata, or sort worktable sizes.
Exceeding 8060 means this inline scenario does not fit, not that saving must fail.
.EXAMPLE
./Measure-SqlServerRowSize.ps1 -NumCount 10 -DateCount 10 -CheckCount 8 `
-ClassCount 20 -ClassBytesPerColumn 40 `
-DescriptionCount 2 -DescriptionBytesPerColumn 200 `
-AttachmentCount 1 -AttachmentBytesPerColumn 300
#>
[CmdletBinding()]
param(
[ValidateRange(0, 1024)]
[int]$NumCount = 0,
[ValidateRange(1, 38)]
[int]$NumPrecision = 18,
[ValidateRange(0, 1024)]
[int]$DateCount = 0,
[ValidateRange(0, 1024)]
[int]$CheckCount = 0,
[ValidateRange(0, 1024)]
[int]$ClassCount = 0,
[ValidateRange(0, 8000)]
[long]$ClassBytesPerColumn = 0,
[ValidateRange(0, 1024)]
[int]$DescriptionCount = 0,
[ValidateRange(0, 2147483647)]
[long]$DescriptionBytesPerColumn = 0,
[ValidateRange(0, 1024)]
[int]$AttachmentCount = 0,
[ValidateRange(0, 2147483647)]
[long]$AttachmentBytesPerColumn = 0,
[ValidateRange(0, 1024)]
[int]$OtherFixedCount = 0,
[ValidateRange(0, 2147483647)]
[long]$OtherFixedBytes = 0,
[ValidateRange(0, 1024)]
[int]$OtherVariableCount = 0,
[ValidateRange(0, 2147483647)]
[long]$OtherVariableBytes = 0
)
$groups = @(
@{ Count = $ClassCount; Bytes = $ClassBytesPerColumn; Parameter = 'ClassBytesPerColumn' }
@{ Count = $DescriptionCount; Bytes = $DescriptionBytesPerColumn; Parameter = 'DescriptionBytesPerColumn' }
@{ Count = $AttachmentCount; Bytes = $AttachmentBytesPerColumn; Parameter = 'AttachmentBytesPerColumn' }
@{ Count = $OtherFixedCount; Bytes = $OtherFixedBytes; Parameter = 'OtherFixedBytes' }
@{ Count = $OtherVariableCount; Bytes = $OtherVariableBytes; Parameter = 'OtherVariableBytes' }
)
foreach ($group in $groups) {
if ($group.Count -gt 0 -and -not $PSBoundParameters.ContainsKey($group.Parameter)) {
throw "Specify -$($group.Parameter) explicitly, including 0 for an empty variable payload."
}
if ($group.Count -eq 0 -and $group.Bytes -ne 0) {
throw "$($group.Parameter) requires a positive column count."
}
}
if ($OtherFixedCount -gt 0 -and $OtherFixedBytes -lt $OtherFixedCount) {
throw 'OtherFixedBytes must include storage for each fixed column; count all bit columns in CheckCount.'
}
$variableCount = $ClassCount + $DescriptionCount + $AttachmentCount + $OtherVariableCount
$columnCount = $NumCount + $DateCount + $CheckCount + $OtherFixedCount + $variableCount
if ($columnCount -lt 1 -or $columnCount -gt 1024) {
throw 'Specify between 1 and 1024 physical columns for one ordinary table.'
}
$decimalBytes = if ($NumPrecision -le 9) { 5 }
elseif ($NumPrecision -le 19) { 9 }
elseif ($NumPrecision -le 28) { 13 }
else { 17 }
[long]$bitBytes = [math]::Ceiling($CheckCount / 8.0)
[long]$fixedBytes = $NumCount * $decimalBytes + $DateCount * 8 + $bitBytes + $OtherFixedBytes
[long]$nullBitmapBytes = 2 + [math]::Ceiling($columnCount / 8.0)
[long]$variableMetadataBytes = if ($variableCount -gt 0) { 2 + 2 * $variableCount } else { 0 }
[long]$managementBytes = 4 + $nullBitmapBytes + $variableMetadataBytes
[long]$variableBytes = $ClassCount * $ClassBytesPerColumn +
$DescriptionCount * $DescriptionBytesPerColumn +
$AttachmentCount * $AttachmentBytesPerColumn + $OtherVariableBytes
[long]$estimatedBytes = $fixedBytes + $managementBytes + $variableBytes
Write-Warning 'Inline estimate only, not a storage guarantee. Include standard columns; verify actual DDL, off-row storage and operations.'
[pscustomobject]@{
Scenario = 'All variable payloads in-row'
ColumnCount = $columnCount
VariableColumnCount = $variableCount
DecimalBytesPerColumn = $decimalBytes
BitBytes = $bitBytes
FixedBytes = $fixedBytes
RowHeaderBytes = 4
NullBitmapBytes = $nullBitmapBytes
VariableMetadataBytes = $variableMetadataBytes
ManagementBytes = $managementBytes
VariablePayloadBytes = $variableBytes
EstimatedInlineBytes = $estimatedBytes
LimitBytes = 8060
RemainingBytes = 8060 - $estimatedBytes
InlineScenarioFits = $estimatedBytes -le 8060
}Get-SqlServerRowDiagnostics.ps1
Get-SqlServerRowDiagnostics.ps1 をダウンロード
Get-SqlServerRowDiagnostics.ps1(読み取り専用の現状診断)
#Requires -Version 7.0
<#
.SYNOPSIS
Reads SQL Server table definitions, allocations, and row payload diagnostics.
.DESCRIPTION
Requires SQL Server 2016 or later and the existing System.Data.SqlClient assembly.
Uses encrypted, certificate-validated connections. Windows/integrated authentication
is the default; Credential selects SQL authentication without a password in the
connection string. Queries are read-only and do not change database/session settings.
Returns one object per existing table. Metadata, Budget, Allocation, Payload and
PhysicalStats have Status, Reason and Data properties. Columns contains catalog
metadata, including nonpersisted computed columns (excluded from storage estimates).
Optional query failures are warnings and Unavailable sections, never zero estimates.
Budget is an uncompressed, all-declared-variable-bytes-inline definition scenario.
It is NOT measured physical storage, a saving guarantee, or a worktable estimate.
It excludes slot arrays, version tags, dropped-column remnants and other internal
record overhead. Unsupported layouts have no budget. MAX string/binary columns
produce a Partial budget: fixed/management bytes remain known, but the declared
variable total, inline total, remaining bytes and fit result are NULL.
Default payload queries inspect at most SampleRows rows with TOP and no ordering;
the cap does not bound bytes, I/O, or runtime. This is not a random or representative
sample, and its maximum is not a table-wide maximum. DATALENGTH includes off-row payload,
NOT physical row bytes or LOB/overflow pointers. Only stored variable string/binary
columns are measured. Per-column averages exclude NULL; row sums treat NULL as zero.
The row maximum is a maximum of sums from individual rows, not a sum of maxima.
Empty sets have RowCount 0 and NULL totals, averages and maxima.
Both sample and full payload scans include only rows visible to the current login;
row-level security may filter rows, while allocation statistics may include more.
Payload statistics are unavailable when a selected variable column is encrypted or
dynamically masked, even with UNMASK permission; masking can alter derived lengths.
Detailed removes the sample limit and requests DETAILED physical statistics for
the base heap/clustered index. It can be expensive and can block or be blocked.
Allocation counts are approximate, and separate queries are not a consistent snapshot.
No cell values, credentials, or connection strings are written to output.
.PARAMETER Server
SQL Server instance/address, at most 128 characters.
.PARAMETER Database
Exact database name, at most 128 characters.
.PARAMETER Table
Exact unqualified table name, at most 128 characters. Do not supply bracket quoting.
.PARAMETER Schema
Exact schema name; defaults to dbo.
.PARAMETER Credential
SQL authentication credentials; omitted for integrated authentication.
.PARAMETER IncludeRelated
Also inspect exact Table_history and Table_deleted names in the same schema.
.PARAMETER Detailed
Scan all rows for payload aggregates and request DETAILED physical statistics.
.PARAMETER SampleRows
Maximum rows inspected in default mode, from 1 to 100000; defaults to 1000.
.PARAMETER CommandTimeout
Per-command timeout in seconds, from 1 to 600; defaults to 30.
.EXAMPLE
./Get-SqlServerRowDiagnostics.ps1 -Server localhost -Database Pleasanter -Table Results
.EXAMPLE
$credential = Get-Credential
./Get-SqlServerRowDiagnostics.ps1 -Server sql.example.com -Database Pleasanter `
-Table Results -Credential $credential -IncludeRelated -Detailed
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) -and $_.Length -le 128 })]
[string]$Server,
[Parameter(Mandatory)]
[ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) -and $_.Length -le 128 })]
[string]$Database,
[Parameter(Mandatory)]
[ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) -and $_.Length -le 128 })]
[string]$Table,
[ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) -and $_.Length -le 128 })]
[string]$Schema = 'dbo',
[System.Management.Automation.PSCredential]$Credential,
[switch]$IncludeRelated,
[switch]$Detailed,
[ValidateRange(1, 100000)]
[int]$SampleRows = 1000,
[ValidateRange(1, 600)]
[int]$CommandTimeout = 30
)
function New-DiagnosticSection {
param([string]$Status, [object]$Data = $null, [string]$Reason = $null)
[pscustomobject]@{ Status = $Status; Reason = $Reason; Data = $Data }
}
function ConvertTo-SqlIdentifier {
param([string]$Name)
'[' + $Name.Replace(']', ']]') + ']'
}
function Invoke-DiagnosticQuery {
param($Connection, [string]$Sql, [hashtable]$Parameters = @{})
$command = $null
$reader = $null
try {
$command = $Connection.CreateCommand()
$command.CommandText = $Sql
$command.CommandTimeout = $CommandTimeout
foreach ($name in $Parameters.Keys) {
$value = $Parameters[$name]
if ($value -is [int]) {
$parameter = $command.Parameters.Add($name, [System.Data.SqlDbType]::Int)
}
else {
$parameter = $command.Parameters.Add($name, [System.Data.SqlDbType]::NVarChar, 128)
}
$parameter.Value = $value
}
$reader = $command.ExecuteReader()
while ($reader.Read()) {
$row = [ordered]@{}
for ($i = 0; $i -lt $reader.FieldCount; $i++) {
$row[$reader.GetName($i)] = if ($reader.IsDBNull($i)) { $null } else { $reader.GetValue($i) }
}
[pscustomobject]$row
}
}
finally {
if ($null -ne $reader) { $reader.Dispose() }
if ($null -ne $command) { $command.Dispose() }
}
}
function Get-DefinitionBudget {
param($Metadata, [object[]]$Columns)
$reasons = [System.Collections.Generic.List[string]]::new()
if ($Metadata.IsMemoryOptimized) { $reasons.Add('Memory-optimized table.') }
if ($Metadata.HasColumnstore) { $reasons.Add('Columnstore index present.') }
if ($Metadata.HasCompression) { $reasons.Add('Base rowstore partition compression enabled.') }
if ($Metadata.HasNonuniqueClusteredIndex) { $reasons.Add('Nonunique clustered index can add a hidden uniqueifier.') }
if ($Metadata.IsFileTable) { $reasons.Add('FileTable layout.') }
if ($Metadata.IsExternal) { $reasons.Add('External table layout.') }
$stored = @($Columns | Where-Object { -not $_.IsComputed -or $_.IsPersisted })
if ($stored.Count -lt 1 -or $stored.Count -gt 1024) { $reasons.Add('Unsupported stored column count.') }
[long]$fixedBytes = 0
[long]$variableBytes = 0
[int]$variableCount = 0
[int]$maxColumnCount = 0
[int]$bitCount = 0
foreach ($column in $stored) {
if ($column.IsSparse -or $column.IsColumnSet) { $reasons.Add('Sparse columns or column set present.') }
if ($column.IsFileStream) { $reasons.Add('FILESTREAM column present.') }
if ($null -ne $column.EncryptionType) { $reasons.Add('Encrypted column present.') }
switch ($column.BaseType) {
'bit' { $bitCount++; break }
{ $_ -in 'decimal', 'numeric' } {
$fixedBytes += if ($column.Precision -le 9) { 5 }
elseif ($column.Precision -le 19) { 9 }
elseif ($column.Precision -le 28) { 13 }
else { 17 }
break
}
'date' { $fixedBytes += 3; break }
{ $_ -in 'time', 'datetime2', 'datetimeoffset' } {
$timeBytes = if ($column.Scale -le 2) { 3 } elseif ($column.Scale -le 4) { 4 } else { 5 }
$fixedBytes += $timeBytes
if ($_ -in 'datetime2', 'datetimeoffset') { $fixedBytes += 3 }
if ($_ -eq 'datetimeoffset') { $fixedBytes += 2 }
break
}
{ $_ -in 'tinyint', 'smallint', 'int', 'bigint', 'real', 'float',
'money', 'smallmoney', 'datetime', 'smalldatetime', 'uniqueidentifier',
'char', 'nchar', 'binary', 'timestamp' } {
$fixedBytes += $column.MaxLength
break
}
{ $_ -in 'varchar', 'nvarchar', 'varbinary' } {
$variableCount++
if ($column.MaxLength -eq -1) { $maxColumnCount++ }
else { $variableBytes += $column.MaxLength }
break
}
default { $reasons.Add("Unsupported storage type: $($column.BaseType).") }
}
}
if ($reasons.Count -gt 0) {
return New-DiagnosticSection -Status Unavailable -Reason (($reasons | Select-Object -Unique) -join ' ')
}
[long]$bitBytes = [math]::Ceiling($bitCount / 8.0)
$fixedBytes += $bitBytes
[long]$nullBytes = 2 + [math]::Ceiling($stored.Count / 8.0)
[long]$variableMetadata = if ($variableCount -gt 0) { 2 + 2 * $variableCount } else { 0 }
[long]$managementBytes = 4 + $nullBytes + $variableMetadata
[long]$fixedAndManagementBytes = $fixedBytes + $managementBytes
$estimatedBytes = if ($maxColumnCount -eq 0) { $fixedAndManagementBytes + $variableBytes } else { $null }
$status = if ($maxColumnCount -gt 0) { 'Partial' } else { 'Available' }
$reason = if ($maxColumnCount -gt 0) {
'MAX columns have no bounded inline definition budget. FixedAndManagementBytes excludes ALL variable payload and off-row references; the overall row size and fit are unknown.'
} else { $null }
New-DiagnosticSection -Status $status -Reason $reason -Data ([pscustomobject]@{
Scenario = 'Uncompressed declared maximum variable payload entirely in-row'
StoredColumnCount = $stored.Count
VariableColumnCount = $variableCount
MaxColumnCount = $maxColumnCount
BitBytes = $bitBytes
FixedBytes = $fixedBytes
RowHeaderBytes = 4
NullBitmapBytes = $nullBytes
VariableMetadataBytes = $variableMetadata
ManagementBytes = $managementBytes
FixedAndManagementBytes = $fixedAndManagementBytes
DeclaredVariablePayloadBytes = if ($maxColumnCount -eq 0) { $variableBytes } else { $null }
EstimatedInlineBytes = $estimatedBytes
LimitBytes = 8060
RemainingBytes = if ($maxColumnCount -eq 0) { 8060 - $estimatedBytes } else { $null }
InlineScenarioFits = if ($maxColumnCount -eq 0) { $estimatedBytes -le 8060 } else { $null }
Caveat = 'Definition scenario only: not physical row size or a guarantee that writes/operations succeed. Excludes off-row pointers, version tags, dropped-column remnants, slot arrays and other internal overhead.'
})
}
function New-PayloadQuery {
param([string]$QualifiedName, [object[]]$Columns, [bool]$FullScan)
$lengths = [System.Collections.Generic.List[string]]::new()
$sums = [System.Collections.Generic.List[string]]::new()
$aggregates = [System.Collections.Generic.List[string]]::new()
foreach ($column in $Columns) {
$alias = ConvertTo-SqlIdentifier "c$($column.ColumnId)"
$identifier = ConvertTo-SqlIdentifier $column.Name
$lengths.Add("CONVERT(bigint, DATALENGTH($identifier)) AS $alias")
$sums.Add("COALESCE($alias, CONVERT(bigint, 0))")
$id = [int]$column.ColumnId
$aggregates.Add("COUNT_BIG($alias) AS [NonNull$id], SUM($alias) AS [Total$id], MAX($alias) AS [Maximum$id]")
}
$top = if ($FullScan) { '' } else { 'TOP (@SampleRows) ' }
$lengthSql = if ($lengths.Count) { $lengths -join ",`n " } else { 'CONVERT(bigint, 0) AS [NoVariablePayload]' }
$sumSql = if ($sums.Count) { $sums -join ' + ' } else { 'CONVERT(bigint, 0)' }
$columnSql = if ($aggregates.Count) { ",`n " + ($aggregates -join ",`n ") } else { '' }
@"
WITH [Sample] AS (
SELECT $top$lengthSql
FROM $QualifiedName
), [PayloadRows] AS (
SELECT *, $sumSql AS [RowPayloadBytes] FROM [Sample]
)
SELECT COUNT_BIG(*) AS [RowCount],
SUM([RowPayloadBytes]) AS [TotalRowPayloadBytes],
AVG(CONVERT(decimal(38,4), [RowPayloadBytes])) AS [AverageRowPayloadBytes],
MAX([RowPayloadBytes]) AS [MaxRowPayloadBytes]$columnSql
FROM [PayloadRows];
"@
}
function Get-TableDiagnostics {
param($Connection, [string]$RequestedTable, [bool]$Required)
$tableSql = @'
SELECT t.object_id AS ObjectId, s.name AS SchemaName, t.name AS TableName,
t.is_memory_optimized AS IsMemoryOptimized, t.is_filetable AS IsFileTable,
t.is_external AS IsExternal,
CONVERT(bit, CASE WHEN EXISTS (
SELECT 1 FROM sys.indexes i WHERE i.object_id = t.object_id AND i.type IN (5, 6)
) THEN 1 ELSE 0 END) AS HasColumnstore,
CONVERT(bit, CASE WHEN EXISTS (
SELECT 1 FROM sys.partitions p
WHERE p.object_id = t.object_id AND p.index_id IN (0, 1) AND p.data_compression <> 0
) THEN 1 ELSE 0 END) AS HasCompression,
CONVERT(bit, CASE WHEN EXISTS (
SELECT 1 FROM sys.indexes i
WHERE i.object_id = t.object_id AND i.index_id = 1 AND i.is_unique = 0
) THEN 1 ELSE 0 END) AS HasNonuniqueClusteredIndex,
(SELECT MIN(i.index_id) FROM sys.indexes i
WHERE i.object_id = t.object_id AND i.index_id IN (0, 1)) AS BaseIndexId
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
WHERE s.name = @Schema AND t.name = @Table;
'@
$columnSql = @'
SELECT c.column_id AS ColumnId, c.name AS Name, ts.name AS TypeSchema,
ut.name AS ActualType, COALESCE(bt.name, ut.name) AS BaseType,
c.max_length AS MaxLength, c.precision AS Precision, c.scale AS Scale,
c.is_nullable AS IsNullable, c.is_computed AS IsComputed,
CONVERT(bit, COALESCE(cc.is_persisted, 0)) AS IsPersisted,
c.is_sparse AS IsSparse, c.is_column_set AS IsColumnSet,
c.is_filestream AS IsFileStream, c.encryption_type AS EncryptionType,
c.is_hidden AS IsHidden, CONVERT(bit, COALESCE(mc.is_masked, 0)) AS IsMasked
FROM sys.columns c
JOIN sys.types ut ON ut.user_type_id = c.user_type_id
JOIN sys.schemas ts ON ts.schema_id = ut.schema_id
LEFT JOIN sys.types bt ON bt.user_type_id = c.system_type_id AND bt.system_type_id = bt.user_type_id
LEFT JOIN sys.computed_columns cc ON cc.object_id = c.object_id AND cc.column_id = c.column_id
LEFT JOIN sys.masked_columns mc ON mc.object_id = c.object_id AND mc.column_id = c.column_id
WHERE c.object_id = @ObjectId
ORDER BY c.column_id;
'@
try {
$tables = @(Invoke-DiagnosticQuery $Connection $tableSql @{ '@Schema' = $Schema; '@Table' = $RequestedTable })
if ($tables.Count -eq 0) {
if ($Required) { throw 'Base table is absent or its metadata is inaccessible.' }
Write-Warning "Related table '$RequestedTable' is absent or its metadata is inaccessible; skipped."
return
}
$metadata = $tables[0]
$columns = @(Invoke-DiagnosticQuery $Connection $columnSql @{ '@ObjectId' = [int]$metadata.ObjectId })
if ($columns.Count -eq 0) { throw 'No visible column metadata.' }
}
catch {
if ($Required) { throw 'Base table definition unavailable: table not found, metadata permission denied, or unsupported SQL Server version.' }
Write-Warning "Related table '$RequestedTable' definition unavailable; skipped."
return
}
$qualifiedName = (ConvertTo-SqlIdentifier $metadata.SchemaName) + '.' + (ConvertTo-SqlIdentifier $metadata.TableName)
$budget = Get-DefinitionBudget $metadata $columns
$allocationSql = @'
SELECT partition_number AS PartitionNumber, index_id AS IndexId,
row_count AS ApproximateRows, in_row_used_page_count AS InRowUsedPages,
row_overflow_used_page_count AS RowOverflowUsedPages,
lob_used_page_count AS LobUsedPages, used_page_count AS UsedPages,
reserved_page_count AS ReservedPages
FROM sys.dm_db_partition_stats
WHERE object_id = @ObjectId AND index_id IN (0, 1)
ORDER BY partition_number;
'@
try {
$partitions = @(Invoke-DiagnosticQuery $Connection $allocationSql @{ '@ObjectId' = [int]$metadata.ObjectId })
if ($partitions.Count -eq 0) { throw 'No allocation statistics returned.' }
$allocation = New-DiagnosticSection -Status Available -Data ([pscustomobject]@{
Partitions = $partitions
ApproximateRows = [long]($partitions | Measure-Object ApproximateRows -Sum).Sum
InRowUsedPages = [long]($partitions | Measure-Object InRowUsedPages -Sum).Sum
RowOverflowUsedPages = [long]($partitions | Measure-Object RowOverflowUsedPages -Sum).Sum
LobUsedPages = [long]($partitions | Measure-Object LobUsedPages -Sum).Sum
UsedPages = [long]($partitions | Measure-Object UsedPages -Sum).Sum
ReservedPages = [long]($partitions | Measure-Object ReservedPages -Sum).Sum
PageBytes = 8192
Scope = 'Base heap/clustered index only; approximate, not a transactionally consistent snapshot.'
})
}
catch {
$allocation = New-DiagnosticSection -Status Unavailable -Reason 'Allocation statistics unavailable (permission, timeout, unsupported storage, or query failure).'
Write-Warning "$qualifiedName allocation statistics unavailable."
}
$payloadColumns = @($columns | Where-Object {
(-not $_.IsComputed -or $_.IsPersisted) -and
$_.BaseType -in 'varchar', 'nvarchar', 'varbinary', 'text', 'ntext', 'image'
})
$payloadFailureReason = 'Payload statistics unavailable (permission, timeout, or query failure).'
try {
if (@($payloadColumns | Where-Object { $_.IsMasked }).Count) {
$payloadFailureReason = 'Payload statistics unavailable: dynamic data masking is not supported, even with UNMASK permission, because derived lengths can be masked.'
throw 'Masked variable columns are not supported.'
}
if (@($columns | Where-Object { $_.IsMasked }).Count -and
@($payloadColumns | Where-Object { $_.IsComputed -and $_.IsPersisted }).Count) {
$payloadFailureReason = 'Payload statistics unavailable: persisted computed variable columns may inherit dynamic data masking from another masked column; inherited masking is not supported, even with UNMASK permission.'
throw 'Potential inherited masking in persisted computed variable columns.'
}
if (@($payloadColumns | Where-Object { $null -ne $_.EncryptionType }).Count) {
$payloadFailureReason = 'Payload statistics unavailable: encrypted variable columns are not supported.'
throw 'Encrypted variable columns are not supported.'
}
$payloadSql = New-PayloadQuery $qualifiedName $payloadColumns ([bool]$Detailed)
$payloadParameters = if ($Detailed) { @{} } else { @{ '@SampleRows' = $SampleRows } }
$payloadRows = @(Invoke-DiagnosticQuery $Connection $payloadSql $payloadParameters)
if ($payloadRows.Count -ne 1) { throw 'Payload aggregate missing.' }
$row = $payloadRows[0]
$columnStats = @(
foreach ($column in $payloadColumns) {
$id = $column.ColumnId
[pscustomobject]@{
ColumnId = $id
Name = $column.Name
NonNullCount = $row."NonNull$id"
NullCount = $row.RowCount - $row."NonNull$id"
TotalBytes = $row."Total$id"
AverageBytes = if ($row."NonNull$id" -gt 0) {
[decimal]$row."Total$id" / [decimal]$row."NonNull$id"
} else { $null }
MaxBytes = $row."Maximum$id"
}
}
)
$payload = New-DiagnosticSection -Status Available -Data ([pscustomobject]@{
Mode = if ($Detailed) { 'FullScan' } else { 'BoundedSample' }
SampleLimit = if ($Detailed) { $null } else { $SampleRows }
RowCount = $row.RowCount
TotalRowPayloadBytes = $row.TotalRowPayloadBytes
AverageRowPayloadBytes = $row.AverageRowPayloadBytes
MaxRowPayloadBytes = $row.MaxRowPayloadBytes
Columns = $columnStats
Scope = 'Stored variable strings/binary and rows visible to the current login only; row-level security may filter even a FullScan. Allocation statistics may include more rows. DATALENGTH includes off-row bytes, not physical row storage. Per-column averages exclude NULL; row sums treat NULL as zero. TOP is unordered, not representative.'
})
}
catch {
$payload = New-DiagnosticSection -Status Unavailable -Reason $payloadFailureReason
Write-Warning "$qualifiedName payload statistics unavailable."
}
$physical = New-DiagnosticSection -Status NotRequested -Reason 'Use -Detailed to request the expensive physical statistics scan.'
if ($Detailed) {
try {
if ($metadata.IsMemoryOptimized -or $metadata.HasColumnstore -or $metadata.IsExternal -or $null -eq $metadata.BaseIndexId) {
throw 'Physical rowstore statistics are not supported for this layout.'
}
$physicalSql = @'
SELECT partition_number AS PartitionNumber, index_id AS IndexId,
alloc_unit_type_desc AS AllocationUnitType, page_count AS PageCount,
record_count AS RecordCount,
CASE WHEN record_count > 0 THEN avg_record_size_in_bytes END AS AverageRecordBytes,
CASE WHEN record_count > 0 THEN min_record_size_in_bytes END AS MinRecordBytes,
CASE WHEN record_count > 0 THEN max_record_size_in_bytes END AS MaxRecordBytes
FROM sys.dm_db_index_physical_stats(DB_ID(), @ObjectId, @IndexId, NULL, 'DETAILED')
WHERE index_id IN (0, 1) AND index_level = 0 AND alloc_unit_type_desc = 'IN_ROW_DATA'
ORDER BY partition_number;
'@
$physicalRows = @(Invoke-DiagnosticQuery $Connection $physicalSql @{
'@ObjectId' = [int]$metadata.ObjectId
'@IndexId' = [int]$metadata.BaseIndexId
})
if ($physicalRows.Count -eq 0) { throw 'No physical statistics returned.' }
$physical = New-DiagnosticSection -Status Available -Data ([pscustomobject]@{
Mode = 'DETAILED'
Partitions = $physicalRows
Scope = 'Base heap/clustered index leaf IN_ROW_DATA records only; excludes off-row payload. RecordCount is not necessarily the logical row count.'
})
}
catch {
$physical = New-DiagnosticSection -Status Unavailable -Reason 'Physical statistics unavailable (permission, timeout, unsupported storage, or query failure).'
Write-Warning "$qualifiedName physical statistics unavailable."
}
}
[pscustomobject]@{
Server = $Server
Database = $Database
Schema = $metadata.SchemaName
Table = $metadata.TableName
ObjectId = $metadata.ObjectId
Metadata = New-DiagnosticSection -Status Available -Data $metadata
Columns = $columns
Budget = $budget
Allocation = $allocation
Payload = $payload
PhysicalStats = $physical
}
}
try {
$builder = [System.Data.SqlClient.SqlConnectionStringBuilder]::new()
}
catch {
throw 'System.Data.SqlClient is required but unavailable in this PowerShell installation.'
}
$builder['Data Source'] = $Server
$builder['Initial Catalog'] = $Database
$builder['Encrypt'] = $true
$builder['TrustServerCertificate'] = $false
$builder['Connect Timeout'] = 15
$builder['Persist Security Info'] = $false
$builder['Application Name'] = 'Get-SqlServerRowDiagnostics'
$builder['Integrated Security'] = $null -eq $Credential
$connection = $null
$password = $null
try {
$connection = [System.Data.SqlClient.SqlConnection]::new($builder.ConnectionString)
if ($null -ne $Credential) {
$password = $Credential.Password.Copy()
$password.MakeReadOnly()
$connection.Credential = [System.Data.SqlClient.SqlCredential]::new($Credential.UserName, $password)
}
try { $connection.Open() }
catch { throw 'SQL Server connection failed. Check connectivity, authentication, and the server certificate trust/name; connection details are not logged.' }
Get-TableDiagnostics $connection $Table $true
if ($IncludeRelated) {
foreach ($suffix in '_history', '_deleted') {
$related = $Table + $suffix
if ($related.Length -gt 128) {
Write-Warning "Related table name exceeds 128 characters for suffix '$suffix'; skipped."
continue
}
Get-TableDiagnostics $connection $related $false
}
}
}
finally {
if ($null -ne $connection) { $connection.Dispose() }
if ($null -ne $password) { $password.Dispose() }
}関連ページ
- データベースのテーブル構成(ベース・_deleted・_history) — 拡張項目の列が
_history・_deletedにも作られる仕組み - CodeDefiner(テーブル作成とコード自動生成) — 列の追加(
_rds)の流れ - DB メンテナンスと SQL での情報取得
- 添付ファイルの保存先