Skip to content

画面別の内部 SQL ​

第5版作成 最終更新 (日本時間)
確認バージョン1.5.2.01.5.8.1

一覧以外のビュー(カレンダー、ガントチャート、バーンダウンチャート、時系列チャート、カンバン、クロス集計、画像ライブラリ、分析チャート)が発行する 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 BYORDER 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 文 の比較表も同じ扱いです。

カレンダー ​

処理フロー ​

text
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() は呼ばれずカードは表示されません。

csharp
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() ​

csharp
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();
}

IssueUtilities.cs#L9060-L9131

日付範囲条件(3 パターンの OR) ​

カレンダーは「表示期間に かかる レコードをすべて取得する」のが特徴です。終了日時列が設定されている場合、次の 3 パターンを OR でつなぎます。終了日時列がない場合は、開始日時列の between @Begin and @End だけになります。

図を読み込み中…

StartTime を開始日時、CompletionTime を終了日時とした場合の条件です。

sql
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 は付与されず、カレンダー上への配置はサーバーから返したあとクライアント側で処理されます。

列エイリアス用途
IssueIdId一意識別子
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)beginend
年(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)。

ガントチャート ​

処理フロー ​

text
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() ​

csharp
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();
}

IssueUtilities.cs#L9788-L9843

GanttUtilities.Where() の日付範囲条件 ​

カレンダーと同じ「3 パターンの OR」構造ですが、生の列名ではなく Def.Sql.StartTimeColumn・CompletionTimeSql() という定義ファイル参照の SQL 式を使います。CompletionTime に「業務日換算」の補正を加えられる設計になっているためです。

csharp
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());
}

GanttUtilities.cs#L34-L58

業務日換算なしの場合の WHERE 句のイメージです。

sql
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(表示日数)から計算されます。

csharp
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 / CalendarLimitGanttLimit(取得件数)
取得する主な列Id・Status・From・To・TitleId・WorkValue・StartTime・CompletionTime・ProgressRate・Status

バーンダウンチャート ​

現在レコードと履歴レコードを UNION で取得し、C# 側で時系列の残作業量を計算します。

処理フロー ​

text
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() ​

csharp
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 のイメージ ​

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" ASC

UNION 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 のパターンが切り替わります。

処理フロー ​

text
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() ​

csharp
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 して取得します。

sql
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 条件が自動で追加されます。横軸列が未設定のレコードは集計から除外されます。

sql
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)
TableTypeNormalAndHistory(UNION ALL)Normal
HorizontalAxisUpdatedTime指定列
WHEREIssueId IN (サブクエリ)フィルタ + 列名 IS NOT NULL
取得行数現在レコード + 全履歴バージョン現在レコードのみ
用途時間経過による値の変化の追跡特定日時列に基づく分布
列エイリアス説明
UpdatedTime または指定列HorizontalAxis横軸の値
IssueIdIdレコード識別子
Ver—バージョン(Histories の場合に意味を持つ)
グループ列グループ列名系列(凡例)の分類
集計値列集計値列名Y 軸の値

カンバン ​

プリザンターのソースでは、カンバンは「Kamban」と表記されています。

処理フロー ​

text
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() はバーンダウンチャート・時系列チャートでも上限値を変えて使われています。

csharp
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

sql
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()) ​

csharp
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

sql
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 がありません。

処理SQLC#
グループ化なし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 軸)KambanGroupByXgroupByXSELECT 列に追加
行(Y 軸)KambanGroupByYgroupByYSELECT 列に追加(空なら追加なし)
集計列KambanValuevalueSELECT 列に追加(空なら追加なし)
集計方法KambanAggregateType—(SQL に影響なし)C# 側の集計方法に影響
表示列数KambanColumns—(SQL に影響なし)HTML の列数レイアウトのみ

カード移動(UpdateByKamban()) ​

カードを別の列にドラッグ&ドロップすると UpdateByKamban() が呼ばれます。

csharp
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

順序種類内容
①SELECTIssueId = KambanId で 1 件取得(コンストラクタ内)
②UPDATEIssues テーブルを更新(移動先の列の値を書き換え)
③SELECT COUNTInRange チェック(再描画前の件数確認)
④SELECTKambanData(更新後のカードデータ全件再取得)

② の UPDATE は通常の issueModel.Update() と同じで、楽観的排他制御の UpdatedTime 条件も付きます(内部で動く SQL 文 の「レコード更新(UPDATE)」を参照)。

sql
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  -- 楽観的排他制御
sql
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  -- 楽観的排他制御
sql
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 が加わります。

処理フロー ​

text
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 列が変わります。

csharp
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" のときは各列名が別名になります。

集計タイプaggregateTypeSELECT 列の 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)。

csharp
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();
csharp
// 日付を年・月・週・日にグループ化する式を生成
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 は付きません。

sql
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 句の両方に入ります。

sql
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"
sql
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"
sql
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)から生成します。

csharp
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;
    }
}

CrosstabUtilities.cs#L67-L83

表示期間の 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 は発行しません。

csharp
// 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;
}

CrosstabUtilities.cs#L35-L65

カンバンとの違い ​

項目カンバンクロス集計
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' 条件を加えたものです。

処理フロー ​

text
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 クラス ​

csharp
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);
}

ImageLibData.cs

発行される SQL のイメージ ​

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)
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" = '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)
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" = '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 BYColumnSorterHash + デフォルトColumnSorterHash + デフォルト
ページサイズGridPageSize(サイト設定)ImageLibPageSize(サイト設定)

ImageLibPageSize はサイト設定の「画像ライブラリの 1 ページの枚数」で、デフォルトは 48 枚(Parameters.General.ImageLibPageSize)です。「さらに読み込む」ボタンを押すと ImageLibNext() が呼ばれ、offset を増やして追加で取得します。

分析チャート ​

分析パーツ(画面上のカード)ごとに、現在レコードと過去時点のスナップショットの SQL を発行します。

処理フロー ​

text
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)。

csharp
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" の例です。

sql
-- ① 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 件だけが取得されます。過去のある時点のスナップショットを再現するための仕組みです。

カットオフ日時の計算 ​

csharp
DateTime.Now.DateAdd(timePeriod: timePeriod, timePeriodValue: (timePeriodValue * -1).ToInt())
timePeriodtimePeriodValueカットオフ日時(現在が 2026/4/15 の場合)
"Months"32026/1/15(3 か月前)
"Months"62025/10/15(6 か月前)
"Days"72026/4/8(7 日前)
"Years"12025/4/15(1 年前)

timePeriodValue = 0(デフォルト)の場合は History SELECT は発行されず、Normal SELECT だけが実行されます。

SELECT 列 ​

列エイリアス説明
IssueIdIdレコード識別子
Ver—バージョン番号
グループ列GroupBy分析パーツの集計グループ
集計値列Value集計対象の値(設定がない場合は省略)

WARNING

分析チャートには InRange によるレコード数チェックがありません。分析パーツの数だけ SQL が発行され、timePeriodValue > 0 の場合は各パーツに History SELECT も加わります。大規模データのサイトで多数の分析パーツを設定する場合は、パフォーマンスに注意してください。

日付の月単位の式は DBMS 別の DateGroupMonthly(PostgreSqlSqls.cs、MySqlSqls.cs)に合わせています。更新日時の式・組み込みパラメータは DBMS ごとの SQL の書き方 を参照してください。

関連ページ ​

変更履歴

第5版記事の確認版を繰り返す表現を整理する
第4版ビューの内部 SQL の日付集計とページ取得をDBMSごとに説明
第3版「内部実装を読む」を 1.5.8.1 のソースで検証して修正
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「内部実装を読む」に画面別の内部 SQL(カレンダー〜分析チャート)を追加