画面別の内部 SQL
一覧以外のビュー(カレンダー、ガントチャート、バーンダウンチャート、時系列チャート、カンバン、クロス集計、画像ライブラリ、分析チャート)が発行する SQL を、ビューごとに整理します。 どのビューも View.Where()・Rds.SelectIssues()・Repository.ExecuteDataSet() / ExecuteTable() という共通の仕組みを使い、SELECT 列・WHERE 条件・テーブルタイプ(通常 / 履歴)・GROUP BY の有無を切り替えてそれぞれの要件を満たしています。
SQL 組み立ての共通アーキテクチャ(SqlSelect、View.Where()、ss.Join()、テーブルタイプなど)と、一覧画面・編集画面の SQL は 内部で動く SQL 文 を参照してください。
対象バージョン
バージョン 1.5.2.0 のソースコードを対象にしています。コード例は要点を抜粋・簡略化したもの、SQL は発行されるクエリのイメージです(例では Issues テーブル、テナント ID 1、サイト ID 42 としています)。
SQL 例の読み方
DBMS 名のない SQL は、3 DBMS で共通の構文を使った説明用の抜粋です。プリザンターは MySQL 接続でも ansi_quotes を設定するため、識別子の二重引用符が使えます。DB のコンソールで直接実行するときの引用符・パラメータの扱いは DBMS ごとの SQL の書き方 を参照してください。... を含む例は実行用 SQL ではありません。
ビューごとの比較
| ビュー | データ取得メソッド | 対象テーブル | 件数・上限チェック | GROUP BY | ORDER BY | ページネーション | 集計・配置の処理 |
|---|---|---|---|---|---|---|---|
| カレンダー | CalendarDataRows() | 通常 | InRangeY(グループ行数と CalendarYLimit)、取得後に InRange(CalendarLimit) | なし | なし | なし(全件) | クライアント側で配置 |
| ガントチャート | GanttDataRows() | 通常 | 取得後に dataRows.Count() <= GanttLimit(COUNT SQL なし) | なし | なし | なし(全件) | クライアント側で配置 |
| バーンダウンチャート | BurnDownDataRows() | 通常 + 履歴(UNION) | InRange(SELECT COUNT、BurnDownLimit) | なし | IssueId, Ver 昇順 | なし(全件) | C# の BurnDown クラスで計算 |
| 時系列チャート | TimeSeriesDataRows() | 横軸が更新履歴なら通常 + 履歴(UNION ALL)、列なら通常 | InRange(SELECT COUNT、TimeSeriesLimit) | なし | なし | なし(全件) | C# で処理 |
| カンバン | KambanData() | 通常 | InRange(SELECT COUNT、KambanLimit) | なし | なし | なし(全件) | C# の LINQ で集計 |
| クロス集計 | CrosstabDataRows() | 通常 | 取得後に InRangeX / InRangeY(C# で種類数) | あり | なし | なし(全件) | SQL の集計関数 |
| 画像ライブラリ | ImageLibData | 通常 + Binaries(INNER JOIN) | SELECT COUNT(件数取得) | なし | ビューのソート設定 + デフォルト | あり(ImageLibPageSize) | — |
| 分析チャート | AnalyDat()(分析パーツごと) | 通常(過去比較時は + 履歴) | なし | なし | なし | なし(全件) | C# で処理 |
件数チェックの方式は大きく 3 種類に分かれます。
図を読み込み中…
カレンダーの件数チェック
カレンダーの CalendarLimit のチェックは SELECT COUNT ではなく、取得したデータ行(dataRows)に対して CalendarUtilities.InRange() で行われます(1.5.8.1 のソースでは CalendarUtilities.cs#L20-L32、IssueUtilities.cs#L9062-L9077)。内部で動く SQL 文 の比較表も同じ扱いです。
カレンダー
処理フロー
GET /items/{siteId}/calendar
└─ IssueUtilities.Calendar() ← L8742 IssueUtilities.cs
├─ CalendarUtilities.InRangeY(choicesCount) グループ行数チェック
│ └─(choicesCount > CalendarYLimit なら以降をスキップ)
├─(InRangeY=true)CalendarDataRows() ← L9060 IssueUtilities.cs
│ └─ Rds.SelectIssues(最小列,
│ WHERE: 日付範囲 3パターン OR + フィルタ)
│ ① SELECT(表示期間内レコード)
└─ CalendarUtilities.InRange(dataRows, CalendarLimit)
② 取得件数チェック(上限超過でカードを非表示)InRangeY は グループ列の選択肢数(Y 軸の行数)を CalendarYLimit と比較します。CalendarYLimit = 0 の場合はチェックなし(無制限)です。InRangeY が false の場合、CalendarDataRows() は呼ばれずカードは表示されません。
public static bool InRangeY(Context context, int choicesCount)
{
// CalendarYLimit == 0 の場合は制限なし
var inRange = Parameters.General.CalendarYLimit == 0
|| choicesCount <= Parameters.General.CalendarYLimit;
if (!inRange)
{
SessionUtilities.Set(
context: context,
message: Messages.TooManyRowCases(
context: context,
data: Parameters.General.CalendarYLimit.ToString()));
}
return inRange;
}CalendarDataRows()
private static EnumerableRowCollection<DataRow> CalendarDataRows(
Context context,
SiteSettings ss,
View view,
Column fromColumn, // 開始日時列(例:"StartTime")
Column toColumn, // 終了日時列(例:"CompletionTime"、null の場合は単一日付)
Column groupBy, // グループ列
DateTime begin, // 表示期間の開始
DateTime end) // 表示期間の終了
{
// ① 日付範囲 WHERE の追加
var where = new SqlWhereCollection();
if (toColumn == null)
{
// 単一日付列(fromColumn のみ)
where.Add(
tableName: "Issues",
raw: $"\"Issues\".\"{fromColumn.ColumnName}\" between @Begin and @End");
}
else
{
// 開始〜終了日時の範囲(3 パターンの OR)
where.Add(or: Rds.IssuesWhere()
.Add(raw: $"\"Issues\".\"{fromColumn.ColumnName}\" between @Begin and @End")
.Add(raw: $"\"Issues\".\"{toColumn.ColumnName}\" between @Begin and @End")
.Add(raw: $"\"Issues\".\"{fromColumn.ColumnName}\"<=@Begin"
+ $" and \"Issues\".\"{toColumn.ColumnName}\">=@End"));
}
// ② 画面フィルタ設定を追加
where = view.Where(context, ss, where: where);
// ③ パラメータに期間を設定
var param = view.Param(context, ss);
param.Add(new SqlParam() { VariableName = "Begin", Value = begin, NoCount = true });
param.Add(new SqlParam() { VariableName = "End", Value = end, NoCount = true });
// ④ SELECT(カレンダー表示に必要な最小限の列)
return Rds.ExecuteTable(context, statements: Rds.SelectIssues(
column: Rds.IssuesTitleColumn(context, ss)
.IssueId(_as: "Id")
.SiteId(_as: "SiteId")
.Status()
.IssuesColumn(columnName: fromColumn.ColumnName, _as: "From")
.IssuesColumn(columnName: toColumn?.ColumnName, _as: "To")
.UpdatedTime()
.ItemTitle(ss.ReferenceType)
.Add(column: groupBy, function: Sqls.Functions.SingleColumn),
join: ss.Join(context, join: where),
where: where,
param: param))
.AsEnumerable();
}日付範囲条件(3 パターンの OR)
カレンダーは「表示期間に かかる レコードをすべて取得する」のが特徴です。終了日時列が設定されている場合、次の 3 パターンを OR でつなぎます。終了日時列がない場合は、開始日時列の between @Begin and @End だけになります。
図を読み込み中…
StartTime を開始日時、CompletionTime を終了日時とした場合の条件です。
WHERE (
"Issues"."StartTime" between @Begin and @End
OR "Issues"."CompletionTime" between @Begin and @End
OR "Issues"."StartTime" <= @Begin AND "Issues"."CompletionTime" >= @End
)
AND EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND ...(画面フィルタ条件)パターン③があることで、表示期間より長くまたがるタスクも漏れなく取得できます。
SELECT 列
一覧画面と異なり、必要最小限の列だけを取得します。ORDER BY は付与されず、カレンダー上への配置はサーバーから返したあとクライアント側で処理されます。
| 列 | エイリアス | 用途 |
|---|---|---|
IssueId | Id | 一意識別子 |
Title(Items テーブル) | ItemTitle | カレンダーに表示するタイトル |
Status | — | ステータスバッジの色 |
| 開始日時列 | From | カレンダー上の配置位置(開始) |
| 終了日時列 | To | カレンダー上の配置位置(終了) |
UpdatedTime | — | キャッシュ制御 |
| グループ列 | — | グループ分け |
表示期間(begin / end)
begin / end は Calendars.BeginDate() / Calendars.EndDate()(Libraries/Requests/Calendars.cs)で計算されます。月表示でも当月 1 日からではなく、当月 1 日を含む週の先頭(FirstDayOfWeek、既定値 1 = 月曜)から 6 週分を取得します。
時間軸(timePeriod) | begin | end |
|---|---|---|
年(Yearly) | 基準日の月の 1 日 | begin の 1 年後の 3 ミリ秒前 |
月(Monthly) | 基準月 1 日を含む週の先頭日 | begin の 43 日後の 3 ミリ秒前 |
週(Weekly) | 基準日を含む週の先頭日 | begin の 8 日後の 3 ミリ秒前 |
FullCalendar 形式のカレンダーでは、画面から送られた CalendarStart / CalendarEnd があればそれを使います(月表示の既定は begin の 43 日後の 3 ミリ秒前)。いずれも最後に UTC へ変換されます。1.5.8.1 のソースで確認しました(Calendars.cs#L14-L119、General.json#L90)。
ガントチャート
処理フロー
GET /items/{siteId}/gantt
└─ IssueUtilities.Gantt() ← L9607 IssueUtilities.cs
├─ GanttDataRows() ← L9788 IssueUtilities.cs
│ └─ Rds.SelectIssues(進捗列多数,
│ WHERE: GanttUtilities.Where() + フィルタ)
│ ① SELECT(表示期間内レコード・COUNT SQL なし)
└─ dataRows.Count() <= Parameters.General.GanttLimit
件数チェック(SELECT COUNT は発行しない)GanttDataRows()
private static EnumerableRowCollection<DataRow> GanttDataRows(
Context context, SiteSettings ss, View view, Column groupBy, Column sortBy)
{
// ① GanttUtilities.Where() を先に設定し、画面フィルタを追加
var where = view.Where(
context, ss,
where: Libraries.ViewModes.GanttUtilities.Where(context, ss));
// ② ガント期間パラメータの設定
var param = view.Param(context, ss);
var start = view.GanttStartDate.ToDateTime().ToUniversal(context);
var end = start.AddDays(view.GanttPeriod.ToInt()).AddMilliseconds(-3);
param.Add(new SqlParam() { VariableName = "Start", Value = start, NoCount = true });
param.Add(new SqlParam() { VariableName = "End", Value = end, NoCount = true });
// ③ SELECT(ガント表示に必要な列)
return Repository.ExecuteTable(context, statements: Rds.SelectIssues(
column: Rds.IssuesTitleColumn(context, ss)
.IssueId()
.WorkValue()
.StartTime()
.CompletionTime()
.ProgressRate()
.Status()
.Owner()
.Updator()
.CreatedTime()
.UpdatedTime()
.ItemTitle(ss.ReferenceType)
.Add(context, column: groupBy, function: Sqls.Functions.SingleColumn)
.Add(context, column: sortBy, function: Sqls.Functions.SingleColumn),
join: ss.Join(context, join: where),
where: where,
param: param))
.AsEnumerable();
}GanttUtilities.Where() の日付範囲条件
カレンダーと同じ「3 パターンの OR」構造ですが、生の列名ではなく Def.Sql.StartTimeColumn・CompletionTimeSql() という定義ファイル参照の SQL 式を使います。CompletionTime に「業務日換算」の補正を加えられる設計になっているためです。
public static SqlWhereCollection Where(Context context, SiteSettings ss)
{
// 3パターン OR:カレンダーと同じ構造
return Rds.IssuesWhere().Add(or: Rds.IssuesWhere()
// パターン①:期間全体を包む(StartTime <= @Start AND CompletionTime >= @End)
.Add(raw: "(({0}) <= @Start and {1} >= @End)".Params(
Def.Sql.StartTimeColumn,
CompletionTimeSql(context: context, ss: ss)))
// パターン②:開始日が期間内(StartTime between @Start and @End)
.Add(raw: "({0}) between @Start and @End".Params(
Def.Sql.StartTimeColumn))
// パターン③:終了日が期間内(CompletionTime between @Start and @End)
.Add(raw: "({0}) between @Start and @End".Params(
CompletionTimeSql(context: context, ss: ss))));
}
// CompletionTime に業務日換算の補正を加えた SQL 式を生成
private static string CompletionTimeSql(Context context, SiteSettings ss)
{
return Def.Sql.CompletionTimeColumn.Replace(
"#DifferenceOfDates#",
TimeExtensions.DifferenceOfDates(
ss.GetColumn(context: context, columnName: "CompletionTime")?
.EditorFormat, minus: true).ToString());
}業務日換算なしの場合の WHERE 句のイメージです。
WHERE (
"Issues"."StartTime" <= @Start AND "Issues"."CompletionTime" >= @End
OR "Issues"."StartTime" between @Start and @End
OR "Issues"."CompletionTime" between @Start and @End
)
AND ...(画面フィルタ・権限条件)表示期間
表示期間は画面設定の GanttStartDate(開始日)と GanttPeriod(表示日数)から計算されます。
var start = view.GanttStartDate.ToDateTime().ToUniversal(context);
var end = start.AddDays(view.GanttPeriod.ToInt()).AddMilliseconds(-3);end は開始日から GanttPeriod 日後の 0 時の 3 ミリ秒前です。AddMilliseconds(-3) で、翌日 0 時ちょうどのレコードを含めずに最終日の終わりまでを範囲にしています(SQL Server の datetime の精度が約 3 ミリ秒のため)。確認したソースでも同じ計算です(IssueUtilities.cs#L10074-L10075)。
SELECT 列
進捗バーの描画やアサイン表示のため、カレンダーより多くの列を取得します。ORDER BY は付与されず、配置はクライアント側で処理されます。
| 列 | 用途 |
|---|---|
IssueId | 一意識別子 |
WorkValue | 工数(バーの長さ) |
StartTime | ガントバーの開始位置 |
CompletionTime | ガントバーの終了位置 |
ProgressRate | 進捗率(バー内の塗りつぶし) |
Status | ステータス色 |
Owner | 担当者表示 |
Updator | 更新者 |
CreatedTime / UpdatedTime | 作成日時・更新日時 |
| グループ列 | グループ分け |
| ソート列 | グループ内のソート |
カレンダーとの違い
| 項目 | カレンダー | ガントチャート |
|---|---|---|
| データ取得メソッド | CalendarDataRows() | GanttDataRows() |
| 日付範囲パラメータ | @Begin / @End | @Start / @End |
| 日付範囲の種類 | 月 / 週 / 日 | 任意の開始日 + 表示日数 |
| FROM / TO 列 | 設定可能(TO は省略可) | StartTime / CompletionTime 固定 |
| 日付列の参照 | 生の列名 | Def.Sql の SQL 式(業務日換算の補正あり) |
| 件数チェック | CalendarYLimit / CalendarLimit | GanttLimit(取得件数) |
| 取得する主な列 | Id・Status・From・To・Title | Id・WorkValue・StartTime・CompletionTime・ProgressRate・Status |
バーンダウンチャート
現在レコードと履歴レコードを UNION で取得し、C# 側で時系列の残作業量を計算します。
処理フロー
GET /items/{siteId}/burndown
└─ IssueUtilities.BurnDown() ← L9845 IssueUtilities.cs
├─ InRange(context, ss, view, BurnDownLimit) ← L11094 IssueUtilities.cs
│ └─ Rds.SelectIssues(COUNT のみ,
│ WHERE: フィルタ)
│ ① SELECT COUNT(件数チェック)
└─(inRange=true の場合のみ)
└─ HtmlBuilder.BurnDown()
└─ BurnDownDataRows() ← L9974 IssueUtilities.cs
├─ Rds.SelectIssues(通常テーブル)
│ ② SELECT(現在レコード)
└─ UNION Rds.SelectIssues(HistoryWithoutFlag)
③ SELECT(履歴レコード)BurnDown() はビューモードが有効かを確認したあと、カンバンと同じ InRange()(カンバン を参照)で件数をチェックし、BurnDownLimit を超えた場合は Messages.TooManyCases を表示してデータ取得をスキップします(IssueUtilities.cs#L9845-L9895)。
BurnDownDataRows()
private static EnumerableRowCollection<DataRow> BurnDownDataRows(
Context context, SiteSettings ss, View view)
{
var where = view.Where(context: context, ss: ss);
var param = view.Param(context: context, ss: ss);
var join = ss.Join(context: context, join: where);
return Repository.ExecuteTable(context, statements: new SqlStatement[]
{
// ② 現在レコード(通常テーブル)
Rds.SelectIssues(
column: Rds.IssuesTitleColumn(context, ss)
.IssueId()
.Ver()
.Title()
.WorkValue()
.StartTime()
.CompletionTime()
.ProgressRate()
.Status()
.Updator()
.CreatedTime()
.UpdatedTime(),
join: join,
where: where,
param: param),
// ③ 履歴レコード(UNION で結合)
Rds.SelectIssues(
unionType: Sqls.UnionTypes.Union, // UNION(重複除去)
tableType: Sqls.TableTypes.HistoryWithoutFlag, // Issues_history テーブル
column: Rds.IssuesTitleColumn(context, ss)
.IssueId(_as: "Id")
.Ver()
.Title()
.WorkValue()
.StartTime()
.CompletionTime()
.ProgressRate()
.Status()
.Updator()
.CreatedTime()
.UpdatedTime(),
join: join,
// WHERE: 現在フィルタに一致する IssueId の履歴レコードのみ
where: Rds.IssuesWhere()
.IssueId_In(sub: Rds.SelectIssues(
column: Rds.IssuesColumn().IssueId(),
where: where)),
param: param,
orderBy: Rds.IssuesOrderBy()
.IssueId() // IssueId 昇順
.Ver()) // バージョン昇順
}).AsEnumerable();
}IssueUtilities.cs#L9974-L10032
発行される SQL のイメージ
-- ② 現在レコード SELECT(通常テーブル)
SELECT
"Issues"."IssueId",
"Issues"."Ver",
"Issues"."Title",
"Issues"."WorkValue",
"Issues"."StartTime",
"Issues"."CompletionTime",
"Issues"."ProgressRate",
"Issues"."Status",
"Issues"."Updator",
"Issues"."CreatedTime",
"Issues"."UpdatedTime"
FROM "Issues"
INNER JOIN "Items" ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND ...(画面フィルタ条件)
UNION
-- ③ 履歴レコード SELECT(Issues_history テーブル)
SELECT
"Issues_history"."IssueId",
"Issues_history"."Ver",
...(同じ列)
FROM "Issues_history"
INNER JOIN "Items" ON ...(同じ JOIN)
WHERE
"Issues_history"."IssueId" IN (
SELECT "Issues"."IssueId"
FROM "Issues"
INNER JOIN "Items" ON ...
WHERE EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND ...(同じフィルタ条件)
)
ORDER BY "Issues_history"."IssueId" ASC, "Issues_history"."Ver" ASCUNION ALL ではなく UNION を使うため、完全に同一内容の行は重複排除されます。ただし通常は Ver が異なるため重複は発生しません。
UNION の目的
BurnDown クラス(Libraries/ViewModes/BurnDown.cs)が UNION 結果の Ver と UpdatedTime を使い、各日付時点での WorkValue の残量 を計算します。これがバーンダウンチャートの縦軸(残作業量)の折れ線になります。
| データ | 役割 |
|---|---|
| 現在レコード(通常テーブル) | 現在の WorkValue・ProgressRate・Status |
| 履歴レコード(履歴テーブル) | 各バージョン更新時点での WorkValue の変化 |
Ver / UpdatedTime | バーンダウン計算の時間軸 |
SELECT 列
| 列 | 説明 |
|---|---|
IssueId | レコードの識別子 |
Ver | バージョン番号(時系列に使用) |
Title | 詳細ポップアップに表示するタイトル |
WorkValue | 作業量(バーンダウンの主軸値) |
StartTime / CompletionTime | 開始・終了日時(横軸範囲に影響) |
ProgressRate | 進捗率(詳細表示用) |
Status | 完了・未完了の判定 |
Updator | 更新者(詳細表示用) |
CreatedTime | 作成日時 |
UpdatedTime | 更新日時(計算の時間軸) |
WARNING
バーンダウンチャートは現在レコードだけでなく すべての履歴レコード を取得します。更新回数が多いレコードや件数が多い場合、取得データが非常に大きくなることがあります。BurnDownLimit で件数を制限しているのはそのためです。
時系列チャート
横軸(HorizontalAxis)の設定によって SQL のパターンが切り替わります。
処理フロー
GET /items/{siteId}/timeseries
└─ IssueUtilities.TimeSeries() ← L10033 IssueUtilities.cs
├─ view.GetTimeSeriesHorizontalAxis() 横軸列を取得
├─ InRange(context, ss, view, TimeSeriesLimit) ← L11094 IssueUtilities.cs
│ └─ Rds.SelectIssues(COUNT のみ)
│ ① SELECT COUNT(件数チェック)
└─(inRange=true の場合のみ)
└─ TimeSeriesDataRows() ← L10212 IssueUtilities.cs
├─(horizontalAxis == "Histories" の場合)
│ TableType=NormalAndHistory
│ WHERE: IssueId IN(サブクエリ)
│ ② SELECT(更新履歴横軸)
└─(横軸 = 列名 の場合)
TableType=Normal
WHERE: フィルタ + 列 IS NOT NULL
③ SELECT(列値横軸)TimeSeries() は横軸を取得できない場合(null)に BadRequest エラーを返し、TimeSeriesLimit を使った InRange() で件数をチェックします(IssueUtilities.cs#L10033-L10095)。
TimeSeriesDataRows()
private static EnumerableRowCollection<DataRow> TimeSeriesDataRows(
Context context, SiteSettings ss, View view,
Column groupBy, Column value, string horizontalAxis)
{
if (groupBy != null && value != null)
{
var historyHorizontalAxis = horizontalAxis == "Histories";
// SELECT 句の組み立て
var column = Rds.IssuesColumn();
if (historyHorizontalAxis)
{
// 横軸 = 更新履歴:UpdatedTime を HorizontalAxis として使用
column.UpdatedTime(_as: "HorizontalAxis");
}
else
{
// 横軸 = 指定列:列の値を HorizontalAxis として使用
column.IssuesColumn(columnName: horizontalAxis, _as: "HorizontalAxis");
}
column.IssueId(_as: "Id")
.Ver()
.Add(context: context, column: groupBy) // グループ列
.Add(context: context, column: value); // 集計値列
var where = view.Where(context: context, ss: ss);
var param = view.Param(context: context, ss: ss);
var join = ss.Join(context, join: new IJoin[] { column, where });
return Repository.ExecuteTable(context, statements: Rds.SelectIssues(
// 横軸が Histories の場合は NormalAndHistory、そうでなければ Normal
tableType: (historyHorizontalAxis
? Sqls.TableTypes.NormalAndHistory
: Sqls.TableTypes.Normal),
column: column,
join: join,
where: historyHorizontalAxis
// Histories:現在フィルタに一致する IssueId の全履歴を取得
? new Rds.IssuesWhereCollection().IssueId_In(sub: Rds.SelectIssues(
column: Rds.IssuesColumn().IssueId(),
join: join,
where: where))
// 列名指定:列が NULL でないレコードのみ
: where.Add(raw: $"\"Issues\".\"{horizontalAxis}\" is not null"),
param: param))
.AsEnumerable();
}
return null;
}IssueUtilities.cs#L10212-L10280
横軸 = 更新履歴(Histories)
NormalAndHistory で Issues テーブルと Issues_history テーブルを UNION ALL して取得します。
SELECT
"Issues"."UpdatedTime" AS "HorizontalAxis", -- 更新日時が横軸
"Issues"."IssueId" AS "Id",
"Issues"."Ver",
"Issues"."Status", -- グループ列(例)
"Issues"."WorkValue" -- 集計値列(例)
FROM "Issues"
INNER JOIN "Items" ON ...
WHERE "Issues"."IssueId" IN (
-- 現在フィルタに一致する IssueId を取得するサブクエリ
SELECT "Issues"."IssueId"
FROM "Issues"
INNER JOIN "Items" ON ...
WHERE EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND ...(画面フィルタ条件)
)
UNION ALL
SELECT
"Issues_history"."UpdatedTime" AS "HorizontalAxis",
"Issues_history"."IssueId" AS "Id",
"Issues_history"."Ver",
"Issues_history"."Status",
"Issues_history"."WorkValue"
FROM "Issues_history"
INNER JOIN "Items" ON ...
WHERE "Issues_history"."IssueId" IN (
-- 同じサブクエリ(通常テーブルのフィルタ結果)
SELECT "Issues"."IssueId" FROM "Issues" ...
)WARNING
横軸を「更新履歴」にすると、フィルタ条件に一致するすべてのレコードの すべての履歴バージョン が取得されます。更新回数や件数が多い場合は取得データが非常に大きくなる可能性があるため、TimeSeriesLimit による件数制限が重要です。
横軸 = 列名(StartTime など)
通常テーブルのみを使い、横軸列の IS NOT NULL 条件が自動で追加されます。横軸列が未設定のレコードは集計から除外されます。
SELECT
"Issues"."StartTime" AS "HorizontalAxis", -- 指定列が横軸
"Issues"."IssueId" AS "Id",
"Issues"."Ver",
"Issues"."Status", -- グループ列(例)
"Issues"."WorkValue" -- 集計値列(例)
FROM "Issues"
INNER JOIN "Items" ON ...
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND ...(画面フィルタ条件)
AND "Issues"."StartTime" IS NOT NULL -- 横軸列が NULL のレコードを除外2 パターンの比較と SELECT 列
| 項目 | 更新履歴(Histories) | 列名指定(例:StartTime) |
|---|---|---|
| TableType | NormalAndHistory(UNION ALL) | Normal |
| HorizontalAxis | UpdatedTime | 指定列 |
| WHERE | IssueId IN (サブクエリ) | フィルタ + 列名 IS NOT NULL |
| 取得行数 | 現在レコード + 全履歴バージョン | 現在レコードのみ |
| 用途 | 時間経過による値の変化の追跡 | 特定日時列に基づく分布 |
| 列 | エイリアス | 説明 |
|---|---|---|
UpdatedTime または指定列 | HorizontalAxis | 横軸の値 |
IssueId | Id | レコード識別子 |
Ver | — | バージョン(Histories の場合に意味を持つ) |
| グループ列 | グループ列名 | 系列(凡例)の分類 |
| 集計値列 | 集計値列名 | Y 軸の値 |
カンバン
プリザンターのソースでは、カンバンは「Kamban」と表記されています。
処理フロー
GET /items/{siteId}/kamban
└─ IssueUtilities.Kamban()
├─ InRange() ─────────── ① 件数チェック SELECT(KambanLimit 以内か確認)
└─ HtmlBuilder.Kamban()
└─ KambanData() ───── ② カードデータ SELECT(GROUP BY なし・全件取得)
ドラッグ&ドロップでカード移動
└─ IssueUtilities.UpdateByKamban()
├─ new IssueModel(...) ── ③ レコード取得 SELECT(1件)
└─ issueModel.Update() ── ④ レコード更新 UPDATE件数チェック(InRange())
カンバンは全件をクライアントに返すため、レコード数が KambanLimit を超えると件数チェックで弾きます。この InRange() はバーンダウンチャート・時系列チャートでも上限値を変えて使われています。
private static bool InRange(Context context, SiteSettings ss, View view, int limit)
{
var where = view.Where(context: context, ss: ss);
var param = view.Param(context: context, ss: ss);
// 件数のみ取得する SELECT COUNT
return Repository.ExecuteScalar_int(
context: context,
statements: Rds.SelectIssues(
column: Rds.IssuesColumn().IssuesCount(),
join: ss.Join(context, join: new IJoin[] { where }),
where: where,
param: param)) <= limit; // limit を超えたら false
}IssueUtilities.cs#L11094-L11113
SELECT COUNT(*) AS "Count"
FROM "Issues"
INNER JOIN "Items" ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND ...(フィルタ条件)戻り値が KambanLimit(Parameters.General.KambanLimit)以下の場合だけカードデータを取得します。超えた場合は件数が上限を超えている旨のメッセージを表示し、カードは表示しません。
WARNING
件数チェックの SELECT と、その後のカードデータの SELECT は別々のクエリです。チェック後にレコードが追加されると、実際には上限を超えた状態でカードが表示される可能性があります。
カードデータ取得(KambanData())
private static IEnumerable<KambanElement> KambanData(
Context context,
SiteSettings ss,
View view,
Column groupByX, // X 軸のグループ列(例:Status)
Column groupByY, // Y 軸のグループ列(例:Owner)
Column value) // 集計値の列(例:WorkValue)
{
// ① SELECT 句:カンバンカードに必要な固定列 + 可変列
var column = Rds.IssuesColumn()
.IssueId() // カードの ID(クリック時のリンク先)
.SiteId()
.Status() // 固定:ロック・完了判定に使用
.ItemTitle(ss.ReferenceType) // カードのタイトル
.Add(context, column: groupByX) // X 軸グループ列(可変)
.Add(context, column: groupByY) // Y 軸グループ列(可変・省略可)
.Add(context, column: value); // 集計値列(可変・省略可)
// ② WHERE 句:ダッシュボードパーツの条件 + 画面フィルタ
var where = ss.DashboardParts?.Any() == true
? ss.DashboardParts[0].View.Where(context, ss)
: new SqlWhereCollection();
where = view.Where(context, ss, where: where);
// ③ クエリパラメータ
var param = view.Param(context, ss);
// ④ クエリ実行(ORDER BY なし・GROUP BY なし)
return Repository.ExecuteTable(
context: context,
statements: Rds.SelectIssues(
column: column,
join: ss.Join(context, join: new IJoin[] { column, where }),
where: where,
param: param))
.AsEnumerable()
.Select(o => new KambanElement()
{
Id = o.Long("IssueId"),
SiteId = o.Long("SiteId"),
Title = o.String("ItemTitle"),
Status = new Status(o.Int("Status")),
GroupX = groupByX?.ConvertIfUserColumn(o), // ユーザー列は名前に変換
GroupY = groupByY?.ConvertIfUserColumn(o),
Value = o.Decimal(value?.ColumnName)
});
}IssueUtilities.cs#L10728-L10784
SELECT
"Issues"."IssueId",
"Issues"."SiteId",
"Issues"."Status",
"Items"."Title" AS "ItemTitle", -- タイトル
"Issues"."Status" AS "Status", -- X 軸(例)
"Issues"."Owner" AS "Owner", -- Y 軸(例)
"Issues"."WorkValue" AS "WorkValue" -- 集計値(例)
FROM "Issues"
INNER JOIN "Items" ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND ...(フィルタ・権限条件)
-- ORDER BY なし
-- GROUP BY なし| 列 | 固定 / 可変 | 用途 |
|---|---|---|
IssueId | 固定 | カードクリック時の URL |
SiteId | 固定 | マルチサイト対応 |
Status | 固定 | カードのロック・完了バッジ判定 |
ItemTitle | 固定 | カードのタイトル表示 |
| groupByX 列 | 可変 | X 軸(列)の振り分けキー |
| groupByY 列 | 可変 | Y 軸(行)の振り分けキー(省略可) |
| value 列 | 可変 | 集計値(合計・平均など) |
カンバンは C# 側でグループ化・集計するため、SQL には GROUP BY がありません。
| 処理 | SQL | C# |
|---|---|---|
| グループ化 | なし | GroupX・GroupY プロパティで LINQ グループ化 |
| 集計(合計・件数) | なし | AggregationType(Count / TotalWorkValue 等)に応じて Sum / Count |
| ソート | なし | カンバン列の定義順・デフォルト選択肢順 |
ビュー設定と SQL の対応
カンバンの表示設定は View クラスのプロパティに保存され、KambanData() の引数になります。未設定の場合は ViewMode 定義のデフォルト(Definition(ss, "Kamban") の Option1 / Option3 など)か、選択肢の先頭が使われます(View.cs#L458-L514。1.5.8.1 でも同じ位置です: View.cs#L458-L514)。X 軸・Y 軸・集計方法・集計列の既定は、それぞれ Option1〜Option4 です。
| ビュー設定項目 | View プロパティ | KambanData() の引数 | SQL 上の効果 |
|---|---|---|---|
| 列(X 軸) | KambanGroupByX | groupByX | SELECT 列に追加 |
| 行(Y 軸) | KambanGroupByY | groupByY | SELECT 列に追加(空なら追加なし) |
| 集計列 | KambanValue | value | SELECT 列に追加(空なら追加なし) |
| 集計方法 | KambanAggregateType | —(SQL に影響なし) | C# 側の集計方法に影響 |
| 表示列数 | KambanColumns | —(SQL に影響なし) | HTML の列数レイアウトのみ |
カード移動(UpdateByKamban())
カードを別の列にドラッグ&ドロップすると UpdateByKamban() が呼ばれます。
public static string UpdateByKamban(Context context, SiteSettings ss)
{
// ① フォームデータ(KambanId + 変更後の値)から IssueModel を構築
// ここで Get() が呼ばれ、レコード 1 件を SELECT
var issueModel = new IssueModel(
context: context,
ss: ss,
issueId: context.Forms.Long("KambanId"), // 移動したカードの ID
formData: context.Forms); // ドロップ先の列の値
// ② バリデーション
var invalid = IssueValidators.OnUpdating(context, ss, issueModel);
switch (invalid.Type)
{
case Error.Types.None: break;
default: return invalid.MessageJson(context: context);
}
// ③ レコードが存在しない場合は競合エラー
if (issueModel.AccessStatus != Databases.AccessStatuses.Selected)
{
return Messages.ResponseDeleteConflicts(context: context).ToJson();
}
// ④ 変更があった場合のみ UPDATE
var updated = issueModel.Updated(context: context);
if (updated)
{
issueModel.VerUp = Versions.MustVerUp(context, ss, baseModel: issueModel);
issueModel.Update(context: context, ss: ss, notice: true);
}
// ⑤ カンバン全体を再取得して JSON で返却
return KambanJson(context, ss, updated: updated);
}IssueUtilities.cs#L10786-L10822
| 順序 | 種類 | 内容 |
|---|---|---|
| ① | SELECT | IssueId = KambanId で 1 件取得(コンストラクタ内) |
| ② | UPDATE | Issues テーブルを更新(移動先の列の値を書き換え) |
| ③ | SELECT COUNT | InRange チェック(再描画前の件数確認) |
| ④ | SELECT | KambanData(更新後のカードデータ全件再取得) |
② の UPDATE は通常の issueModel.Update() と同じで、楽観的排他制御の UpdatedTime 条件も付きます(内部で動く SQL 文 の「レコード更新(UPDATE)」を参照)。
UPDATE "Issues"
SET "Updator" = @_U,
"UpdatedTime" = GETDATE(),
"Status" = @Status_0, -- ← ドロップ先の Status 値
...
WHERE "Issues"."SiteId" = @SiteId_0
AND "Issues"."IssueId" = @IssueId_0
AND "Issues"."UpdatedTime" = @UpdatedTime_0 -- 楽観的排他制御UPDATE "Issues"
SET "Updator" = @ipU,
"UpdatedTime" = CURRENT_TIMESTAMP,
"Status" = @Status_0, -- ← ドロップ先の Status 値
...
WHERE "Issues"."SiteId" = @SiteId_0
AND "Issues"."IssueId" = @IssueId_0
AND "Issues"."UpdatedTime" = @UpdatedTime_0 -- 楽観的排他制御UPDATE "Issues"
SET "Updator" = @ipU,
"UpdatedTime" = CURRENT_TIMESTAMP(3),
"Status" = @Status_0, -- ← ドロップ先の Status 値
...
WHERE "Issues"."SiteId" = @SiteId_0
AND "Issues"."IssueId" = @IssueId_0
AND "Issues"."UpdatedTime" = @UpdatedTime_0 -- 楽観的排他制御UPDATE 後は KambanJson() → KambanData() で、更新した 1 枚だけでなく 全カードを再取得 します(差分更新ではなく全件置換)。
クロス集計
GROUP BY で DB 側に集計を任せる唯一のビューです。X 軸が datetime 型の場合は、日付のグループ化式と表示期間の WHERE が加わります。
処理フロー
GET /items/{siteId}/crosstab
└─ IssueUtilities.Crosstab()
└─ CrosstabDataRows() ← L9454 IssueUtilities.cs
├─(X 軸が datetime 以外)
│ └─ Rds.SelectIssues(X+Y グループ列 + 集計列,
│ GROUP BY: X + Y)
│ ① SELECT(GROUP BY あり)
└─(X 軸が datetime 型)
├─ CrosstabUtilities.DateGroup() 日付グループ化式を生成
├─ CrosstabUtilities.Where() 表示期間 WHERE を生成
└─ Rds.SelectIssues(日付式 + Y + 集計列,
GROUP BY: 日付式 + Y)
② SELECT(日付グループ化 GROUP BY)集計列(CrosstabColumns())
集計列は CrosstabColumns() 拡張メソッドで生成されます。集計タイプと Y 軸の設定で SELECT 列が変わります。
private static SqlColumnCollection CrosstabColumns(
this SqlColumnCollection self,
Context context, SiteSettings ss, View view,
Column groupByY, List<Column> columns,
Column value, string aggregateType)
{
if (view.CrosstabGroupByY != "Columns")
{
// Y 軸が通常列(ステータス・担当者など)の場合
// Y 軸グループ列と、集計値列(別名 "Value")を追加
return self
.WithItemTitleCoalesced(context, ss, column: groupByY)
.Add(context, column: value,
_as: "Value",
function: Sqls.Function(aggregateType)); // COUNT / SUM / AVG / MIN / MAX
}
else
{
// Y 軸が "Columns"(列ごとに横展開)の場合
// columns に含まれる各列を、列名を別名にして集計関数付きで追加
columns.ForEach(column =>
self.Add(
context: context,
column: column,
_as: column.ColumnName,
function: Sqls.Function(aggregateType)));
return self;
}
}IssueUtilities.cs#L9570-L9607(1.5.8.1 では IssueUtilities.cs#L9846-L9878)
件数のための特別な分岐はなく、aggregateType を Sqls.Function() で集計関数に変換し、value 列にそのまま適用します(Sqls.cs#L49-L61)。集計列(value)が未設定のときは、呼び出し元の Crosstab() が value を IssueId、aggregateType を "Count" に置き換えます(IssueUtilities.cs#L9505-L9516)。別名 "Value" が付くのは Y 軸が通常列のときだけで、Y 軸が "Columns" のときは各列名が別名になります。
| 集計タイプ | aggregateType | SELECT 列の SQL(集計値が WorkValue の例) |
|---|---|---|
| 件数 | "Count" | COUNT("Issues"."WorkValue") AS "Value"(集計列が未設定なら COUNT("Issues"."IssueId")) |
| 合計 | "Total" | SUM("Issues"."WorkValue") AS "Value" |
| 平均 | "Average" | AVG("Issues"."WorkValue") AS "Value" |
| 最小 | "Min" | MIN("Issues"."WorkValue") AS "Value" |
| 最大 | "Max" | MAX("Issues"."WorkValue") AS "Value" |
CrosstabDataRows()
X 軸列の型(datetime か否か)で処理が分岐します(IssueUtilities.cs#L9454-L9570)。
var column = Rds.IssuesColumn()
.WithItemTitleCoalesced(context, ss, column: groupByX) // X 軸グループ列
.CrosstabColumns(context, ss, view, // 集計列(SUM・COUNT など)
groupByY, columns, value, aggregateType);
var where = view.Where(context, ss);
// GROUP BY X 軸 + Y 軸
var groupBy = Rds.IssuesGroupBy()
.WithItemTitleCoalesced(context, ss, column: groupByX)
.WithItemTitleCoalesced(context, ss, column: groupByY);
dataRows = Repository.ExecuteTable(context, statements: Rds.SelectIssues(
column: column,
join: ss.Join(context, join: new IJoin[] { column, where, groupBy }),
where: where,
param: param,
groupBy: groupBy)) // ← GROUP BY あり
.AsEnumerable();// 日付を年・月・週・日にグループ化する式を生成
var dateGroup = CrosstabUtilities.DateGroup(context, ss,
column: groupByX, timePeriod: timePeriod);
var column = Rds.IssuesColumn()
.Add(dateGroup, _as: groupByX.ColumnName) // 日付グループ式を SELECT に追加
.CrosstabColumns(context, ss, view,
groupByY, columns, value, aggregateType);
// 日付範囲 WHERE(表示期間に応じた絞り込み)
var where = view.Where(context, ss,
where: CrosstabUtilities.Where(context, ss,
column: groupByX, timePeriod: timePeriod, month: month));
// GROUP BY 日付グループ式 + Y 軸
var groupBy = Rds.IssuesGroupBy()
.Add(dateGroup)
.WithItemTitleCoalesced(context, ss, column: groupByY);
dataRows = Repository.ExecuteTable(context, statements: Rds.SelectIssues(
column: column,
join: ss.Join(context, join: new IJoin[] { column, where, groupBy }),
where: where,
param: param,
groupBy: groupBy)) // ← GROUP BY あり
.AsEnumerable();発行される SQL のイメージ
X 軸に「ステータス」、Y 軸に「担当者」、集計に「件数」を設定し、集計列を指定していない場合です。ORDER BY は付きません。
SELECT
"Issues"."Status", -- X 軸グループ
"Issues"."Owner", -- Y 軸グループ
COUNT("Issues"."IssueId") AS "Value"
FROM "Issues"
INNER JOIN "Items" ON ...
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND ...(画面フィルタ条件)
GROUP BY
"Issues"."Status",
"Issues"."Owner"X 軸に日付型の列(例:StartTime)、時間軸に「月」を設定した場合(SQL Server)です。月単位のグループ化式が SELECT 句と GROUP BY 句の両方に入ります。
SELECT
-- 月単位グループ化式(DB 方言に依存)
CONVERT(nvarchar, DATEPART(yyyy, "Issues"."StartTime")) + '/'
+ RIGHT('0' + CONVERT(nvarchar, DATEPART(mm, "Issues"."StartTime")), 2)
AS "StartTime",
"Issues"."Owner",
COUNT("Issues"."IssueId") AS "Value"
FROM "Issues"
INNER JOIN ...
WHERE
"Issues"."StartTime" between '2025-04-01' and '2026-03-31' -- 12 か月分
AND ...(画面フィルタ条件)
GROUP BY
CONVERT(nvarchar, DATEPART(yyyy, "Issues"."StartTime")) + '/'
+ RIGHT('0' + CONVERT(nvarchar, DATEPART(mm, "Issues"."StartTime")), 2),
"Issues"."Owner"SELECT
-- 月単位グループ化式(DB 方言に依存)
to_char("Issues"."StartTime", 'YYYY/MM')
AS "StartTime",
"Issues"."Owner",
COUNT("Issues"."IssueId") AS "Value"
FROM "Issues"
INNER JOIN ...
WHERE
"Issues"."StartTime" between '2025-04-01' and '2026-03-31' -- 12 か月分
AND ...(画面フィルタ条件)
GROUP BY
to_char("Issues"."StartTime", 'YYYY/MM'),
"Issues"."Owner"SELECT
-- 月単位グループ化式(DB 方言に依存)
date_format("Issues"."StartTime", '%Y/%m')
AS "StartTime",
"Issues"."Owner",
COUNT("Issues"."IssueId") AS "Value"
FROM "Issues"
INNER JOIN ...
WHERE
"Issues"."StartTime" between '2025-04-01' and '2026-03-31' -- 12 か月分
AND ...(画面フィルタ条件)
GROUP BY
date_format("Issues"."StartTime", '%Y/%m'),
"Issues"."Owner"日付グループ化式(CrosstabUtilities.DateGroup())
データベース方言に応じた日付グループ化の SQL 式を、context.Sqls の定義(DateGroupYearly / DateGroupMonthly / DateGroupWeekly / DateGroupDaily)から生成します。
public static string DateGroup(
Context context, SiteSettings ss, Column column, string timePeriod)
{
var columnBracket = "\"{0}\".\"{1}\"".Params(column.TableName(), column.Name);
switch (timePeriod)
{
case "Yearly": return context.Sqls.DateGroupYearly.Params(columnBracket);
case "Monthly": return context.Sqls.DateGroupMonthly.Params(columnBracket);
case "Weekly":
var part = context.Sqls.DateGroupWeeklyPart.Params(columnBracket);
return context.Sqls.DateGroupWeekly.Params(part);
case "Daily": return context.Sqls.DateGroupDaily.Params(columnBracket);
default: return null;
}
}表示期間の WHERE(CrosstabUtilities.Where())
X 軸が datetime 型の場合、CrosstabUtilities.Where() が時間軸に応じた between 条件を生成し、画面に表示する列数分のデータだけを取得します(CrosstabUtilities.cs#L101-L152)。
| 時間軸 | 範囲の開始 | 範囲の終了 |
|---|---|---|
年(Yearly) | year.AddYears(-11)(year は基準月の年の 1 月 1 日) | year.AddYears(1).AddMilliseconds(-3) |
月(Monthly) | month.AddMonths(-11) | month.AddMonths(1).AddMilliseconds(-3) |
週(Weekly) | end.AddDays(-77)(end は WeeklyEndDate(month)) | end.AddDays(7).AddMilliseconds(-3) |
日(Daily) | month(選択月の 1 日) | month.AddMonths(1).AddMilliseconds(-3)(月末) |
いずれも ToUniversal(context) で UTC に変換されます。
WARNING
CrosstabUtilities.Where() は SQL パラメータ(@変数名)ではなく、日時を yyyy/MM/dd HH:mm:ss.fff 形式の リテラル文字列として直接 SQL に埋め込み ます。型の決まったタイムスタンプ文字列なので SQL インジェクションのリスクはありませんが、View.Where() とは異なるアプローチです。
種類数のチェック(InRangeX / InRangeY)
X 軸・Y 軸の値の種類が多すぎる場合に備え、取得結果に対して C# の LINQ で種類数を数えます。SELECT COUNT は発行しません。
// X 軸の種類数が CrosstabXLimit を超えたら警告
public static bool InRangeX(Context context, EnumerableRowCollection<DataRow> dataRows)
{
return dataRows.Select(o => o.String("GroupByX")).Distinct().Count()
<= Parameters.General.CrosstabXLimit;
}
// Y 軸の種類数が CrosstabYLimit を超えたら警告
public static bool InRangeY(Context context, EnumerableRowCollection<DataRow> dataRows)
{
return dataRows.Select(o => o.String("GroupByY")).Distinct().Count()
<= Parameters.General.CrosstabYLimit;
}カンバンとの違い
| 項目 | カンバン | クロス集計 |
|---|---|---|
| SELECT 列 | IssueId・Title・Status + X/Y/Value 列 | X/Y グループ列 + 集計値列 |
| GROUP BY | なし | あり |
| 集計の実装場所 | C#(LINQ) | SQL の集計関数 |
| 件数チェック | SELECT COUNT(InRange) | 取得後に C# で種類数(InRangeX / InRangeY) |
| 日付軸 | なし | あり(年・月・週・日) |
画像ライブラリ
一覧画面と同じ仕組み(データ + 件数の 2 本、ソート、ページネーション)に、Binaries テーブルへの INNER JOIN と BinaryType = 'Images' 条件を加えたものです。
処理フロー
GET /items/{siteId}/imagelib
└─ IssueUtilities.ImageLib()
└─ new ImageLibData(context, ss, view, offset, pageSize)
├─ Rds.Select(データ取得,
│ JOIN: Items + Binaries INNER JOIN,
│ WHERE: フィルタ + BinaryType='Images')
│ ① SELECT(画像付きレコード)
└─ Rds.SelectCount(同じ WHERE)
② SELECT COUNT(件数取得)ImageLibData クラス
public ImageLibData(
Context context, SiteSettings ss, View view,
int offset = 0, int pageSize = 0)
{
var idColumnBracket = $"\"{Rds.IdColumn(ss.ReferenceType)}\"";
// ① SELECT 句:レコード ID・タイトル・画像 Guid
var column = new SqlColumnCollection()
.Add(columnBracket: idColumnBracket,
tableName: ss.ReferenceType, _as: "Id")
.ItemTitle(ss.ReferenceType)
.Add(tableName: "Binaries", columnBracket: "\"Guid\"");
// ② WHERE 句:画面フィルタ + BinaryType = 'Images' 絞り込み
var where = view.Where(context, ss)
.Binaries_BinaryType("Images"); // ← 画像のみに絞り込む
// ③ ORDER BY 句:ビューのソート設定
var orderBy = view.OrderBy(context, ss);
// ④ JOIN 句:Binaries テーブルを INNER JOIN
var joinExpression = $"\"Binaries\".\"ReferenceId\"="
+ $"\"{ss.ReferenceType}\".{idColumnBracket}";
var join = ss.Join(context, join: new IJoin[] { column, where, orderBy })
.Add(
tableName: "\"Binaries\"",
joinType: SqlJoin.JoinTypes.Inner,
joinExpression: joinExpression); // ← 画像を持つレコードのみ
// ⑤ クエリ実行(データ + 件数)
var dataSet = Repository.ExecuteDataSet(context, statements: new SqlStatement[]
{
Rds.Select(tableName: ss.ReferenceType, dataTableName: "Main",
column: column, join: join, where: where, orderBy: orderBy,
offset: offset, pageSize: pageSize),
Rds.SelectCount(tableName: ss.ReferenceType, join: join, where: where)
});
DataRows = dataSet.Tables["Main"].AsEnumerable();
TotalCount = Rds.Count(dataSet);
}発行される SQL のイメージ
-- ① データ取得
SELECT
"Issues"."IssueId" AS "Id",
"Items"."Title" AS "ItemTitle",
"Binaries"."Guid"
FROM "Issues"
INNER JOIN "Items" ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")
INNER JOIN "Binaries" ON "Binaries"."ReferenceId" = "Issues"."IssueId"
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND "Binaries"."BinaryType" = N'Images' -- 画像のみ
AND ...(画面フィルタ条件)
ORDER BY "Issues"."UpdatedTime" DESC, "Issues"."IssueId" DESC
OFFSET 0 ROWS FETCH NEXT 48 ROWS ONLY
-- ② 件数取得
SELECT COUNT(*) "Count"
FROM "Issues"
INNER JOIN ...(同じ JOIN)
WHERE ...(同じ WHERE)-- ① データ取得
SELECT
"Issues"."IssueId" AS "Id",
"Items"."Title" AS "ItemTitle",
"Binaries"."Guid"
FROM "Issues"
INNER JOIN "Items" ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")
INNER JOIN "Binaries" ON "Binaries"."ReferenceId" = "Issues"."IssueId"
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND "Binaries"."BinaryType" = 'Images' -- 画像のみ
AND ...(画面フィルタ条件)
ORDER BY "Issues"."UpdatedTime" DESC, "Issues"."IssueId" DESC
OFFSET 0 ROWS FETCH NEXT 48 ROWS ONLY
-- ② 件数取得
SELECT COUNT(*) "Count"
FROM "Issues"
INNER JOIN ...(同じ JOIN)
WHERE ...(同じ WHERE)-- ① データ取得
SELECT
"Issues"."IssueId" AS "Id",
"Items"."Title" AS "ItemTitle",
"Binaries"."Guid"
FROM "Issues"
INNER JOIN "Items" ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")
INNER JOIN "Binaries" ON "Binaries"."ReferenceId" = "Issues"."IssueId"
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND "Binaries"."BinaryType" = 'Images' -- 画像のみ
AND ...(画面フィルタ条件)
ORDER BY "Issues"."UpdatedTime" DESC, "Issues"."IssueId" DESC
LIMIT 48 OFFSET 0
-- ② 件数取得
SELECT COUNT(*) "Count"
FROM "Issues"
INNER JOIN ...(同じ JOIN)
WHERE ...(同じ WHERE)Binaries への INNER JOIN のため、次の 2 点に注意してください。
- 画像を 1 枚も持たないレコードは結果から除外されます。
- 同じレコードに複数の画像がある場合は、画像ごとに 1 行返ります(1 レコードが複数行になります)。
一覧画面との違いとページング
| 項目 | 一覧画面(GridData) | 画像ライブラリ(ImageLibData) |
|---|---|---|
| SELECT 列 | GridColumns(一覧表示列) | Id・Title・Binaries.Guid のみ |
| JOIN | 列・WHERE・ORDER BY に応じた自動 JOIN | 上記 + Binaries INNER JOIN |
| WHERE | フィルタ・権限 | フィルタ・権限 + BinaryType = 'Images' |
| ORDER BY | ColumnSorterHash + デフォルト | ColumnSorterHash + デフォルト |
| ページサイズ | GridPageSize(サイト設定) | ImageLibPageSize(サイト設定) |
ImageLibPageSize はサイト設定の「画像ライブラリの 1 ページの枚数」で、デフォルトは 48 枚(Parameters.General.ImageLibPageSize)です。「さらに読み込む」ボタンを押すと ImageLibNext() が呼ばれ、offset を増やして追加で取得します。
分析チャート
分析パーツ(画面上のカード)ごとに、現在レコードと過去時点のスナップショットの SQL を発行します。
処理フロー
GET /items/{siteId}/analy
└─ IssueUtilities.Analy() ← L10307 IssueUtilities.cs
└─(InRange チェックなし)
└─ view.AnalyPartSettings.ForEach(analyPart =>
└─ AnalyDat() ← L10430 IssueUtilities.cs
├─ Rds.SelectIssues(Normal,
│ WHERE: IssueId IN (フィルタ) [+ UpdatedTime <= 過去日時])
│ ① SELECT(現在または過去時点レコード)
└─ Rds.SelectIssues(History, ← timePeriodValue > 0 の場合のみ
WHERE: IssueId IN + Ver = MAX(Ver))
② SELECT(過去時点での最新バージョン)Analy() は InRange を呼ばず、view.AnalyPartSettings の分析パーツごとにグループ列(GroupBy)・集計対象(AggregationTarget)・TimePeriodValue(過去何単位前か)・TimePeriod(Months / Days などの単位)を取り出して AnalyDat() を呼びます(IssueUtilities.cs#L10372-L10430)。
AnalyDat()
分析パーツ 1 枚につき 2 本の SQL を組み立てます。2 本目の History SELECT は timePeriodValue > 0 の場合だけ発行されます(_using: timePeriodValue > 0)。
private static Libraries.ViewModes.AnalyData AnalyDat(
Context context, SiteSettings ss, View view,
AnalyPartSetting analyPartSetting,
Column groupBy, decimal timePeriodValue, string timePeriod,
Column aggregationTarget)
{
if (groupBy != null)
{
// SELECT 句:Id・Ver・グループ列・集計値列
var column = Rds.IssuesColumn()
.IssuesColumn(columnName: Rds.IdColumn("Issues"), _as: "Id")
.IssuesColumn(columnName: "Ver")
.IssuesColumn(columnName: groupBy.ColumnName, _as: "GroupBy");
if (aggregationTarget != null)
{
column.IssuesColumn(columnName: aggregationTarget.ColumnName, _as: "Value");
}
var where = view.Where(context: context, ss: ss);
// timePeriodValue > 0 の場合:過去日時のカットオフを WHERE に追加
if (timePeriodValue > 0)
{
where.Issues_UpdatedTime(
value: DateTime.Now.DateAdd(
timePeriod: timePeriod,
timePeriodValue: (timePeriodValue * -1).ToInt()), // 過去に遡る
_operator: "<="); // 指定時点以前の更新のみ
}
var param = view.Param(context: context, ss: ss);
var join = ss.Join(context, join: new IJoin[] { column, where });
var dataSet = Repository.ExecuteDataSet(context, statements: new SqlStatement[]
{
// ① Normal SELECT(現在または過去時点のレコード)
Rds.SelectIssues(
dataTableName: "Normal",
column: column,
join: join,
where: new Rds.IssuesWhereCollection()
.IssueId_In(sub: Rds.SelectIssues(
column: Rds.IssuesColumn().IssueId(),
join: join,
where: where)), // サブクエリで IssueId を絞り込む
param: param),
// ② History SELECT(timePeriodValue > 0 の場合のみ実行)
Rds.SelectIssues(
dataTableName: "History",
tableType: Sqls.TableTypes.History,
column: column,
join: join,
where: new Rds.IssuesWhereCollection()
.IssueId_In(sub: Rds.SelectIssues(
column: Rds.IssuesColumn().IssueId(),
join: join,
where: where))
// Ver = MAX(Ver) per IssueId
.Ver(sub: Rds.SelectIssues(
tableType: Sqls.TableTypes.History,
_as: "b",
column: Rds.IssuesColumn().Ver(tableName: "b",
function: Sqls.Functions.Max),
where: Rds.IssuesWhere().IssueId(tableName: "b",
raw: "\"Issues\".\"IssueId\""),
groupBy: Rds.IssuesGroupBy().IssueId(tableName: "b"))),
param: param,
_using: timePeriodValue > 0) // ← 0 なら発行しない
});
return new Libraries.ViewModes.AnalyData(
analyPartSetting: analyPartSetting,
dataSet: dataSet);
}
return null;
}IssueUtilities.cs#L10430-L10510
発行される SQL のイメージ
timePeriodValue = 3、timePeriod = "Months" の例です。
-- ① Normal SELECT
SELECT
"Issues"."IssueId" AS "Id",
"Issues"."Ver",
"Issues"."Status" AS "GroupBy", -- グループ列(例:Status)
"Issues"."WorkValue" AS "Value" -- 集計値列(例:WorkValue)
FROM "Issues"
INNER JOIN "Items" ON ...
WHERE "Issues"."IssueId" IN (
SELECT "Issues"."IssueId"
FROM "Issues"
INNER JOIN "Items" ON ...
WHERE EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = 1)
AND "Issues"."SiteId" = 42
AND ...(画面フィルタ条件)
AND "Issues"."UpdatedTime" <= '2026-01-15 00:00:00' -- 3か月前のカットオフ
)
-- ② History SELECT(過去時点での最新バージョン)
SELECT
"Issues_history"."IssueId" AS "Id",
"Issues_history"."Ver",
"Issues_history"."Status" AS "GroupBy",
"Issues_history"."WorkValue" AS "Value"
FROM "Issues_history"
INNER JOIN "Items" ON ...
WHERE "Issues_history"."IssueId" IN (
-- 同じサブクエリ(Normal SELECT と同じ条件)
SELECT "Issues"."IssueId" FROM "Issues" ... WHERE ... AND "UpdatedTime" <= 'カットオフ'
)
AND "Issues_history"."Ver" = (
-- 各 IssueId に対して履歴テーブル内での最大 Ver
SELECT MAX("b"."Ver")
FROM "Issues_history" "b"
WHERE "b"."IssueId" = "Issues_history"."IssueId"
GROUP BY "b"."IssueId"
)History SELECT の Ver = MAX(Ver) per IssueId 条件により、各レコードについて指定時点(カットオフ日時)での最新バージョン 1 件だけが取得されます。過去のある時点のスナップショットを再現するための仕組みです。
カットオフ日時の計算
DateTime.Now.DateAdd(timePeriod: timePeriod, timePeriodValue: (timePeriodValue * -1).ToInt())timePeriod | timePeriodValue | カットオフ日時(現在が 2026/4/15 の場合) |
|---|---|---|
"Months" | 3 | 2026/1/15(3 か月前) |
"Months" | 6 | 2025/10/15(6 か月前) |
"Days" | 7 | 2026/4/8(7 日前) |
"Years" | 1 | 2025/4/15(1 年前) |
timePeriodValue = 0(デフォルト)の場合は History SELECT は発行されず、Normal SELECT だけが実行されます。
SELECT 列
| 列 | エイリアス | 説明 |
|---|---|---|
IssueId | Id | レコード識別子 |
Ver | — | バージョン番号 |
| グループ列 | GroupBy | 分析パーツの集計グループ |
| 集計値列 | Value | 集計対象の値(設定がない場合は省略) |
WARNING
分析チャートには InRange によるレコード数チェックがありません。分析パーツの数だけ SQL が発行され、timePeriodValue > 0 の場合は各パーツに History SELECT も加わります。大規模データのサイトで多数の分析パーツを設定する場合は、パフォーマンスに注意してください。
日付の月単位の式は DBMS 別の DateGroupMonthly(PostgreSqlSqls.cs、MySqlSqls.cs)に合わせています。更新日時の式・組み込みパラメータは DBMS ごとの SQL の書き方 を参照してください。