EF Core 8 と SQL Server の Temporal Table を使っていると、StartTime/EndTime(システム生成の期間列)が FromSqlRaw の「SELECT *」では取れないことがあります。Edition差に見えても原因はテーブル定義。確実に取得する書き方と確認ポイントを解説します。
現象:StartTime / EndTime が SELECT * の結果に出てこない
EF Core 8 で SQL Server を使い、更新時に自動生成される列として期間列(StartTime / EndTime)をマッピングしているケースを想定します。代表的な設定は次のようなものです。
modelBuilder.Entity<MyEntity>(entity =>
{
entity.Property(e => e.StartTime).ValueGeneratedOnAddOrUpdate();
entity.Property(e => e.EndTime).ValueGeneratedOnAddOrUpdate();
});
LINQ クエリで取得すると値が入っているのに、次のように生SQLで SELECT * を実行した場合だけ「StartTime / EndTime が返らない」「列が存在しないように見える」といった差が出ることがあります。
var rows = await context.MyEntities
.FromSqlRaw("SELECT * FROM dbo.MyTable WHERE Id = {0}", id)
.ToListAsync();
さらに厄介なのは、環境(例:開発機の SQL Server Developer Edition と、別環境の Enterprise/Standard など)によって挙動が違うように見える点です。結論から言うと、ここで疑うべきは Edition ではありません。
結論:Edition の違いではなく「Temporal Table の期間列が HIDDEN になっている」
SQL Server の Developer Edition は、機能面では Enterprise Edition 相当として提供されることが多く、単純に「Developer だから列が返らない」ということは基本的に起きません。挙動が違うときは、たいてい スキーマ(テーブル定義)の差 が原因です。
Temporal Table(システムバージョン管理テーブル)では、行の有効期間を表す 2 つの期間列を持ちます。この期間列に HIDDEN 属性 が付いていると、SQL Server は次の挙動をとります。
- HIDDEN 列は
SELECT *で返らない - ただし列自体は存在し、列名を明示すれば取得できる
つまり「SELECT * の結果に出てこない = 列が無い」ではなく、見えていないだけ という状態です。
| 誤解しやすいポイント | 実際に起きていること | 典型的な原因 |
|---|---|---|
| Developer Edition だと列が返らない? | Edition ではなく DDL(テーブル定義) の差 | 期間列が HIDDEN で作られている |
| SSMS の結果に列がない | SELECT * が返さないだけで列は存在 | PERIOD FOR SYSTEM_TIME の期間列が HIDDEN |
| EF が取得できているのに生SQLでは取れない | EF の通常クエリは SELECT * を使わず列名を列挙する | 生SQL側だけ SELECT * に依存している |
Temporal Table の StartTime / EndTime が「普通の列」と違う理由
Temporal Table の期間列(例:StartTime / EndTime、ValidFrom / ValidTo など)は、単なる監査列ではなく、システムバージョン管理の中核です。行の「現在有効な期間」と「過去バージョンの期間」を SQL Server が自動で管理します。
期間列に関して、設計・実装で押さえるべき違いを表にまとめます。
| 項目 | 内容 | アプリ側の影響 |
|---|---|---|
| 値の生成 | SQL Server が自動生成(INSERT/UPDATE 時に更新) | アプリで値をセットしない(EF は生成列として扱う) |
| 用途 | 行が有効だった期間を表す(履歴追跡のキー) | 監査・差分分析・タイムトラベルクエリに使える |
| HIDDEN | SELECT * などワイルドカードから除外される属性 | 生SQLで SELECT * を使うと「無いように見える」 |
Temporal Table を後付けで有効化する場合、既存アプリが SELECT * や列名省略 INSERT をしていても壊れないよう、期間列を HIDDEN にする運用はよくあります。結果として「環境Aは HIDDEN」「環境Bは Visible」のようにズレが生まれると、今回のような混乱が発生します。
最初にやるべき切り分け:列の存在と “Hidden フラグ” を確認する
「本当に列が無いのか」「Hidden 扱いで見えていないのか」を、SQL Server のメタデータから確認します。SSMS の表示やツールの見え方だけで判断しないのがコツです。
sys.columns で is_hidden を確認する
SELECT
c.name,
c.column_id,
c.is_hidden,
c.generated_always_type_desc
FROM sys.columns AS c
WHERE c.object_id = OBJECT_ID(N'dbo.MyTable')
ORDER BY c.column_id;
ポイントは次のとおりです。
is_hidden = 1なら、その列はSELECT *では返りませんgenerated_always_type_descがAS_ROW_START/AS_ROW_ENDであれば、Temporal の期間列である可能性が高いです
期間列(PERIOD)として登録されているかを確認する
期間列の列名が StartTime/EndTime なのか、SysStartTime/SysEndTime なのかは環境で異なることがあります。期間列を特定したい場合は次のように確認します。
SELECT
t.name AS table_name,
p.start_column_id,
sc.name AS start_column_name,
p.end_column_id,
ec.name AS end_column_name
FROM sys.tables AS t
INNER JOIN sys.periods AS p
ON p.object_id = t.object_id
INNER JOIN sys.columns AS sc
ON sc.object_id = t.object_id AND sc.column_id = p.start_column_id
INNER JOIN sys.columns AS ec
ON ec.object_id = t.object_id AND ec.column_id = p.end_column_id
WHERE t.object_id = OBJECT_ID(N'dbo.MyTable');
この結果で出てきた列が、いわゆる StartTime / EndTime(有効期間の開始・終了)です。ここで列名が想定と違う場合、EF Core 側のマッピングや生SQL側の列名指定がズレている可能性もあります。
最後に “明示列指定” で取れるか確認する
Hidden が疑わしいときは、いったん次のように列名を明示して SELECT してみてください。列が存在するなら取得できます。
SELECT
Id,
/* ...必要な列... */
StartTime,
EndTime
FROM dbo.MyTable
WHERE Id = 1;
もし列名が違う場合は、期間列の実名に置き換えるか、別名(AS)で揃えます。
SELECT
Id,
/* ...必要な列... */
StartTime = SysStartTime,
EndTime = SysEndTime
FROM dbo.MyTable
WHERE Id = 1;
対処方法:環境差を無視して確実に取得するなら「列名を明示する」
もっとも安全で、長期的にもおすすめできる解決策は SELECT * をやめる ことです。特に EF Core の FromSqlRaw / FromSqlInterpolated は「あなたが書いた SQL をそのまま実行する」ため、ワイルドカードに依存すると環境差を吸収できません。
EF Core で確実に StartTime / EndTime を取りたい場合は、次のように必要列を列挙します。
var rows = await context.MyEntities
.FromSqlRaw(@"
SELECT
[Id],
[Name],
[StartTime],
[EndTime]
FROM [dbo].[MyTable]
WHERE [Id] = {0}", id)
.AsNoTracking()
.ToListAsync();
期間列の実際の列名が違う場合は、SQL 側で別名を付けて EF のプロパティ名に合わせる と安定します。
var rows = await context.MyEntities
.FromSqlRaw(@"
SELECT
[Id],
[Name],
[StartTime] = [SysStartTime],
[EndTime] = [SysEndTime]
FROM [dbo].[MyTable]
WHERE [Id] = {0}", id)
.AsNoTracking()
.ToListAsync();
なぜ “列名明示” が最強なのか
SELECT * を避けることは一般論としても推奨されますが、Temporal Table + EF Core の文脈では特に効果が大きいです。
- HIDDEN 列の有無に左右されない
- 将来列が追加されても、意図しない列が混入しない(DTO/Entity の破壊的変更を避けられる)
- 列の順序変更に影響されない(特に
SqlDataReaderを直接扱うケースで事故が減る) - 読み取りたい列=必要なデータが明確になり、パフォーマンスの説明もしやすい
EF Core の “通常の LINQ” では起きにくい理由
EF Core が生成する SQL は、基本的に SELECT * ではなく列名を列挙します。そのため期間列が HIDDEN でも、マッピングしているなら明示的に参照して取得できます。今回問題になるのは「生SQL側だけが SELECT * に依存している」状況です。
対処方法:HIDDEN を外して “見える状態” に揃える(スキーマ統一)
次の条件に当てはまるなら、HIDDEN を外してしまう(Visible に統一する)のも現実的です。
- 運用上、SSMS などで
SELECT *を多用しており、期間列も常に見える方が便利 - 複数環境で DDL がバラバラになっており、これを機に統一したい
- 期間列を “監査ログ的に” 表示する要件があり、隠す理由が薄い
期間列の Hidden フラグは、次のように変更できます。
ALTER TABLE dbo.MyTable
ALTER COLUMN StartTime DROP HIDDEN;
ALTER TABLE dbo.MyTable
ALTER COLUMN EndTime DROP HIDDEN;
逆に Hidden にしたい場合は ADD HIDDEN です。
ALTER TABLE dbo.MyTable
ALTER COLUMN StartTime ADD HIDDEN;
ALTER TABLE dbo.MyTable
ALTER COLUMN EndTime ADD HIDDEN;
実施後、SELECT * の結果に期間列が出るかを確認します。
SELECT TOP (10) *
FROM dbo.MyTable
ORDER BY Id DESC;
注意点として、これは DDL 変更です。大きいテーブルではスキーマ変更に伴うロックが発生することがあります。特に本番環境では、必ず検証環境で確認し、バックアップやメンテナンス時間を確保してください。
おすすめの折衷案:ビューで “見せる列” を固定する
「基盤テーブルは HIDDEN のまま維持したい。でもアプリや分析用途では StartTime/EndTime も簡単に取りたい」という場合、ビューを用意してそこに必要列を明示する のがきれいな落としどころになります。
CREATE VIEW dbo.vMyTable
AS
SELECT
Id,
Name,
StartTime,
EndTime
FROM dbo.MyTable;
これなら、アプリ側は SELECT * を使うとしても “ビューの列” が返るため、環境差が出にくくなります。
var rows = await context.MyEntities
.FromSqlRaw("SELECT * FROM dbo.vMyTable WHERE Id = {0}", id)
.AsNoTracking()
.ToListAsync();
ビューを挟むメリットは、列名の別名付け(AS)や、アプリに見せたくない内部列の遮蔽、将来のスキーマ変更の吸収ができる点です。運用で “インターフェースを固定したい” 場合に強力です。
環境差の再発を防ぐ:Edition ではなく “スキーマ差” を管理する
今回のような差分は、「どこかの環境で手作業で DDL を変えた」「移行スクリプトが一部だけ適用されていない」「Temporal 化の手順が環境ごとに違う」といった理由で起こりがちです。再発防止のために、次の観点で運用を見直すのがおすすめです。
| 観点 | やること | 効果 |
|---|---|---|
| DDL の統一 | EF Core Migrations からスクリプトを生成し、全環境に同じ順で適用する | 環境差の根本原因を減らす |
| 差分検知 | 定期的に sys.columns などから列定義をダンプして比較する | Hidden/Nullable/型のズレを早期発見 |
| 生SQLの統制 | SELECT * を禁止(レビュー基準化)し、列名明示を徹底する | Temporal に限らず保守性が上がる |
| 観測性 | EF のログで “実際に投げている SQL” を環境間で比較できるようにする | 「どのSQLが差を生むか」を短時間で特定 |
切り分けに効く:EF Core のログで “実際のSQL” を見る
「EF が何を投げているか」を確認すると、SELECT * と列挙の差、列名の別名、WHERE 条件の違いなどが一発でわかります。開発・検証環境ではログ出力を有効にしておくと、こうした問題の解決が速くなります。
// 例:DbContextOptionsBuilder 側でログを出す(開発用途)
optionsBuilder
.EnableSensitiveDataLogging()
.LogTo(Console.WriteLine);
本番での詳細ログは機密情報の取り扱いに注意が必要ですが、少なくとも「どのSQLが実行されたか」を追える状態にしておくと、環境差調査の難易度が下がります。
よくある質問
Hidden 列を “SELECT * でも返す” オプションはない?
ありません。Hidden はワイルドカード選択(*)から除外するための属性なので、取得したいなら 列名を明示する 必要があります。これはツールや ORM の問題というより、SQL Server の仕様に沿った動きです。
StartTime/EndTime が取得できないとき、EF のプロパティ値はどうなる?
SQL が列を返さなければ、EF Core はそのプロパティを埋められません。非NULL列であっても、C# 側では既定値(DateTime なら 0001-01-01)のように見えることがあります。期間列を使って監査や差分を判定している場合、ここで静かにロジックが壊れることがあるため、生SQLは必ず列名明示 を徹底するのが安全です。
HIDDEN を外しても Temporal の仕組み自体は壊れない?
期間列の自動管理(システムバージョン管理)と、Hidden の有無は別の概念です。Hidden を外すのは「SELECT * で見えるかどうか」を変えるだけです。ただし DDL 変更である以上、ロックや運用影響は発生し得ます。適用手順は環境規模に合わせて慎重に進めてください。
まとめ:安全策は “列名明示”、根本対策は “スキーマ統一”
- StartTime/EndTime が
SELECT *に出ないのは、Edition 差ではなく 期間列が HIDDEN である可能性が高い - 環境差を無視して確実に取るなら、
SELECT *をやめて列名を明示 する - 運用として揃えるなら、HIDDEN を外す(またはビューで公開列を固定する)
- 再発防止は「DDL を統一して適用」「メタデータで差分検知」「EF の生成SQLを観測」が効果的

コメント