Skip to content

DB メンテナンスと SQL での情報取得 ​

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

データベースを直接扱う運用作業のうち、次の 4 つをまとめます。共通の作業は SQL Server・PostgreSQL・MySQL の例を掲載し、孤立ユーザーの SID の修復は SQL Server 専用の手順として説明します。

やりたいこと方法
添付ファイルだけ削除したときに残るデータを消す拡張 SQL で Binaries_deleted から削除
リストア後に Implem.CodeDefiner.dll _rds がログインエラーになる孤立ユーザーの SID を使ってログインを作り直す
サイトごとの通知設定を一覧したいSiteSettings の JSON を DBMS ごとの JSON 関数で展開
Wiki をリンク項目のマスタにしたとき、SQL で項目を扱いたいWiki 本文を行・列に分解し、位置に応じて列に戻す

Binaries_deleted に残る添付ファイルを削除する ​

INFO

BinaryStorage.json の Provider が Rds の場合にのみ有効な方法です。Local で運用している場合は対応できません。

なぜ残るのか ​

添付ファイルは Binaries テーブルに 1 ファイル 1 レコードで格納され、実体は Bin 列にバイナリで入っています(SQL Server では image 型。Binaries_Bin.json)。API でやり取りするときは BASE64 ですが、DB には BASE64 の文字列ではなくバイナリのまま保存されます。添付ファイルを削除すると、そのレコードは Binaries_deleted テーブルに移動します。

既定では、移動後のレコードは何にも使われません。添付ファイルの詳細設定「履歴に存在するファイルは削除しない」を ON にすれば履歴からアクセスできますが、既定は OFF なので、DB に残っているのに触る手段がないデータ になり、DB 肥大化の原因になります。

レコードを削除してゴミ箱からも削除すれば紐づくデータは全削除されますが、「添付ファイルだけ削除してレコードは残す」「削除してもゴミ箱からは消さない」運用は多いはずです(ゴミ箱はサイトの管理権限がないと操作できません)。なお 1.5.8.1 には、BackgroundService.json の DeleteTrashBox を true にすると、DeleteTrashBoxRetentionPeriod(既定 90 日)を過ぎたゴミ箱のデータを DeleteTrashBoxTime の時刻に削除する機能があります(既定は無効。BackgroundService.json)。この処理は Items・Issues・Results・Sites・Wikis などの _deleted テーブルに加えて Binaries_deleted も、更新日時が保持期間より古い行を物理削除します(DeleteTrashBoxTimer.cs)。一定期間後に消えればよいなら、下の拡張 SQL の代わりにこの設定を使えます。

拡張 SQL で削除する ​

レコード更新後に、そのレコードの Binaries_deleted のデータを削除する拡張 SQL を、公式マニュアル に従ってファイルとして配置します。

json
{
    "Description": "レコード更新後に削除済み添付ファイルをレコードごと消す",
    "OnUpdated": true,
    "CommandText": "-- Write an arbitrary SQL statement."
}

SQL ファイル名は DeletedBinariesOnUpdatedDelete.json.sql です。

sql
DELETE FROM
    [Binaries_deleted]
WHERE
    [ReferenceId] = {{Id}}
sql
DELETE FROM
    "Binaries_deleted"
WHERE
    "ReferenceId" = {{Id}}
sql
DELETE FROM
    `Binaries_deleted`
WHERE
    `ReferenceId` = {{Id}}

SQL の中の二重波括弧で囲んだ Id は、実行時にレコード ID に置き換わります(ExtendedSql.cs)。この例は全サイトで実行されます。サイト ID で対象を限定する方法は前述のマニュアルを参照してください。「履歴に存在するファイルは削除しない」を ON にしているサイトを除外したい場合は、Sites テーブルの SiteSettings と Results_history / Issues_history テーブルを組み合わせて条件を組めば対応できます(RDBMS 側で JSON を扱う必要があります)。

TIP

SQL Server の Express 版は DB サイズに 10GB の上限があるため、特に効果があります。ただし添付ファイルを多く扱う場合は、どうしても Rds モードで運用したい場合を除き、Local モードでの運用がおすすめです。

リストア後に _rds が実行できないとき ​

サーバー移行などで DB のバックアップを別環境にリストアした後、Implem.CodeDefiner.dll _rds を実行すると 'Implem.Pleasanter_Owner' はログインできませんでした。 と表示される場合の対処です。原因は Implem.Pleasanter_Owner と Implem.Pleasanter_User が 孤立ユーザー になっていることです。

INFO

SQL Server 限定の方法です。MySQL や PostgreSQL では使えません。2024/12/01 時点で、公式マニュアルの方法には今後非推奨となる方法が含まれていたため、こちらの方法を紹介しています。

1. 孤立ユーザーの SID を調べる ​

SSMS などで次を実行します。Implem.Pleasanter_Owner と Implem.Pleasanter_User の 2 行が表示されるので、それぞれの SID を控えます。

sql
USE [master];
SELECT dp.type_desc, dp.sid, dp.name AS user_name
FROM sys.database_principals AS dp
LEFT JOIN sys.server_principals AS sp
    ON dp.sid = sp.sid
WHERE sp.sid IS NULL
    AND dp.authentication_type_desc = 'INSTANCE';

2. 同じ SID でログインを作成する ​

控えた SID を指定してログインを新規作成し、リストアした DB のユーザーと紐づけます。パスワードは Rds.json に記載しているものを使います。

sql
USE [master];
CREATE LOGIN [Implem.Pleasanter_Owner]
WITH PASSWORD = <Rds.jsonに記載しているImplem.Pleasanter_Ownerのパスワード>,
SID = <取得したImplem.Pleasanter_OwnerのSID>;
CREATE LOGIN [Implem.Pleasanter_User]
WITH PASSWORD = <Rds.jsonに記載しているImplem.Pleasanter_Userのパスワード>,
SID = <取得したImplem.Pleasanter_UserのSID>;

その後、再度 Implem.CodeDefiner.dll _rds を実行します。

通知設定を SQL で取り出す ​

JSON の配列展開には SQL Server の OPENJSON、PostgreSQL の jsonb_array_elements、MySQL 8.0 の JSON_TABLE を使います。SQL Server の OPENJSON は互換性レベル 130 以上、後述の順序付き STRING_SPLIT(第 3 引数が 1 の例)は SQL Server 2022 以降が必要です。

サイトの設定は [Sites] テーブルの [SiteSettings] カラムに JSON で格納されており、通知設定は $.Notifications に配列で入っています。権限や設定の棚卸しに使えます。

sql
SELECT
 *
FROM
 [Sites]
CROSS APPLY
 OPENJSON([SiteSettings], '$.Notifications')
sql
SELECT s.*, n.value
FROM "Sites" s
CROSS JOIN LATERAL jsonb_array_elements(s."SiteSettings"::jsonb->'Notifications') n(value);
sql
SELECT s.*, JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, ']')) AS value
FROM `Sites` s
CROSS JOIN JSON_TABLE(s.`SiteSettings`, '$.Notifications[*]' COLUMNS (
    ord FOR ORDINALITY
)) n;

通知設定の JSON の中身 ​

定義は Notification.cs にあります。NULL の項目(使われていない項目)は JSON に格納されません。

プロパティ内容
IdID
Type通知の種類(下表)
Prefix / Subjectタイトル前のプレフィックス / タイトル
Address / CcAddress / BccAddress送信先(Chatwork などの場合は URL)/ CC / BCC
Tokenトークン(Chatwork などの場合)
MethodTypeHttpClient のメソッド(1: Get、2: Post、3: Put、4: Delete)
Encoding / MediaType / Headers / BodyHttpClient のエンコード / メディアタイプ / ヘッダ / ボディ
UseCustomFormat / Formatカスタムデザインの使用有無 / フォーマット
MonitorChangesColumns変更を検出するカラム(配列)
BeforeCondition / AfterCondition変更前 / 変更後に使うビュー ID(未設定は 0)
Expression変更前・変更後の結合(1: Or、2: And)
AfterCreate / AfterUpdate / AfterDelete / AfterCopy / AfterBulkUpdate / AfterBulkDelete / AfterImport実行条件(作成後・更新後・削除後・コピー後・一括更新後・一括削除後・インポート後)
Disabled無効
Type の値種類
1Mail
2Slack
3ChatWork
4Line
5LineGroup
6Teams
7RocketChat
8InCircle
9HttpClient
10LineWorks

Type の値は 1.5.8.1 のソースの Notification.Types で確認しています(Notification.cs)。

項目ごとに展開するクエリ ​

sql
SELECT
 [SiteId],
 [Title],
 JSON_VALUE(value, '$.Id') AS [Id],
 JSON_VALUE(value, '$.Type') AS [Type],
 JSON_VALUE(value, '$.Prefix') AS [Prefix],
 JSON_VALUE(value, '$.Subject') AS [Subject],
 JSON_VALUE(value, '$.Address') AS [Address],
 JSON_VALUE(value, '$.CcAddress') AS [CcAddress],
 JSON_VALUE(value, '$.BccAddress') AS [BccAddress],
 JSON_VALUE(value, '$.Token') AS [Token],
 JSON_VALUE(value, '$.MethodType') AS [MethodType],
 JSON_VALUE(value, '$.Encoding') AS [Encoding],
 JSON_VALUE(value, '$.MediaType') AS [MediaType],
 JSON_VALUE(value, '$.Headers') AS [Headers],
 JSON_VALUE(value, '$.UseCustomFormat') AS [UseCustomFormat],
 JSON_VALUE(value, '$.Format') AS [Format],
 JSON_VALUE(value, '$.Body') AS [Body],
 JSON_QUERY(value, '$.MonitorChangesColumns') AS [MonitorChangesColumns],
 JSON_VALUE(value, '$.BeforeCondition') AS [BeforeCondition],
 JSON_VALUE(value, '$.AfterCondition') AS [AfterCondition],
 JSON_VALUE(value, '$.Expression') AS [Expression],
 JSON_VALUE(value, '$.AfterCreate') AS [AfterCreate],
 JSON_VALUE(value, '$.AfterUpdate') AS [AfterUpdate],
 JSON_VALUE(value, '$.AfterDelete') AS [AfterDelete],
 JSON_VALUE(value, '$.AfterCopy') AS [AfterCopy],
 JSON_VALUE(value, '$.AfterBulkUpdate') AS [AfterBulkUpdate],
 JSON_VALUE(value, '$.AfterBulkDelete') AS [AfterBulkDelete],
 JSON_VALUE(value, '$.AfterImport') AS [AfterImport],
 JSON_VALUE(value, '$.Disabled') AS [Disabled]
FROM
 [Sites]
CROSS APPLY
 OPENJSON([SiteSettings], '$.Notifications')
sql
SELECT
 s."SiteId",
 s."Title",
 n.value->>'Id' AS "Id",
 n.value->>'Type' AS "Type",
 n.value->>'Prefix' AS "Prefix",
 n.value->>'Subject' AS "Subject",
 n.value->>'Address' AS "Address",
 n.value->>'CcAddress' AS "CcAddress",
 n.value->>'BccAddress' AS "BccAddress",
 n.value->>'Token' AS "Token",
 n.value->>'MethodType' AS "MethodType",
 n.value->>'Encoding' AS "Encoding",
 n.value->>'MediaType' AS "MediaType",
 n.value->>'Headers' AS "Headers",
 n.value->>'UseCustomFormat' AS "UseCustomFormat",
 n.value->>'Format' AS "Format",
 n.value->>'Body' AS "Body",
 n.value->'MonitorChangesColumns' AS "MonitorChangesColumns",
 n.value->>'BeforeCondition' AS "BeforeCondition",
 n.value->>'AfterCondition' AS "AfterCondition",
 n.value->>'Expression' AS "Expression",
 n.value->>'AfterCreate' AS "AfterCreate",
 n.value->>'AfterUpdate' AS "AfterUpdate",
 n.value->>'AfterDelete' AS "AfterDelete",
 n.value->>'AfterCopy' AS "AfterCopy",
 n.value->>'AfterBulkUpdate' AS "AfterBulkUpdate",
 n.value->>'AfterBulkDelete' AS "AfterBulkDelete",
 n.value->>'AfterImport' AS "AfterImport",
 n.value->>'Disabled' AS "Disabled"
FROM "Sites" s
CROSS JOIN LATERAL jsonb_array_elements(s."SiteSettings"::jsonb->'Notifications') n(value);
sql
SELECT
 s.`SiteId`,
 s.`Title`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Id'))) AS `Id`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Type'))) AS `Type`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Prefix'))) AS `Prefix`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Subject'))) AS `Subject`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Address'))) AS `Address`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].CcAddress'))) AS `CcAddress`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].BccAddress'))) AS `BccAddress`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Token'))) AS `Token`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].MethodType'))) AS `MethodType`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Encoding'))) AS `Encoding`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].MediaType'))) AS `MediaType`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Headers'))) AS `Headers`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].UseCustomFormat'))) AS `UseCustomFormat`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Format'))) AS `Format`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Body'))) AS `Body`,
 JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].MonitorChangesColumns')) AS `MonitorChangesColumns`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].BeforeCondition'))) AS `BeforeCondition`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].AfterCondition'))) AS `AfterCondition`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Expression'))) AS `Expression`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].AfterCreate'))) AS `AfterCreate`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].AfterUpdate'))) AS `AfterUpdate`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].AfterDelete'))) AS `AfterDelete`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].AfterCopy'))) AS `AfterCopy`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].AfterBulkUpdate'))) AS `AfterBulkUpdate`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].AfterBulkDelete'))) AS `AfterBulkDelete`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].AfterImport'))) AS `AfterImport`,
 JSON_UNQUOTE(JSON_EXTRACT(s.`SiteSettings`, CONCAT('$.Notifications[', n.ord - 1, '].Disabled'))) AS `Disabled`
FROM `Sites` s
CROSS JOIN JSON_TABLE(s.`SiteSettings`, '$.Notifications[*]' COLUMNS (
    ord FOR ORDINALITY
)) n;

これを WITH 句に入れて応用できます。たとえば Chatwork 宛ての通知の、通知先ルーム ID を取り出すクエリは次のとおりです(Address の URL を / で分割した 6 番目)。

sql
WITH [Notifications] AS (
 SELECT
  [SiteId],
  [Title],
  JSON_VALUE(value, '$.Id') AS [Id],
  JSON_VALUE(value, '$.Type') AS [Type],
  JSON_VALUE(value, '$.Address') AS [Address]
  -- 他の列は上のクエリと同じ
 FROM
  [Sites]
 CROSS APPLY
  OPENJSON([SiteSettings], '$.Notifications')
)
SELECT
 [SiteId],
 [Title],
 [Id] AS [NotificationId],
 value AS [NotificationRoomId]
FROM
 [Notifications]
CROSS APPLY
 STRING_SPLIT ([Address], '/', 1)
WHERE
 [Type] IN (3)
 AND ordinal IN (6)
sql
SELECT s."SiteId", s."Title",
    n.value->>'Id' AS "NotificationId",
    split_part(n.value->>'Address', '/', 6) AS "NotificationRoomId"
FROM "Sites" s
CROSS JOIN LATERAL jsonb_array_elements(s."SiteSettings"::jsonb->'Notifications') n(value)
WHERE n.value->>'Type' = '3'
  AND array_length(string_to_array(n.value->>'Address', '/'), 1) >= 6;
sql
SELECT s.`SiteId`, s.`Title`, n.`Id` AS `NotificationId`,
    SUBSTRING_INDEX(SUBSTRING_INDEX(n.`Address`, '/', 6), '/', -1) AS `NotificationRoomId`
FROM `Sites` s
CROSS JOIN JSON_TABLE(s.`SiteSettings`, '$.Notifications[*]' COLUMNS (
    `Id` INT PATH '$.Id',
    `Type` INT PATH '$.Type',
    `Address` TEXT PATH '$.Address'
)) n
WHERE n.`Type` = 3
  AND CHAR_LENGTH(n.`Address`) - CHAR_LENGTH(REPLACE(n.`Address`, '/', '')) >= 5;

Wiki のリンク項目マスタを SQL で取り出す ​

リンク項目 のマスタには Wiki も使えます。キーに ReferenceId 以外を使える、項目が少なければ表示のパフォーマンスがよい、といった利点がある一方、SQL で直接扱いにくいのが難点です。次のクエリで Wiki の本文を行・列に分解できます。列の名前は Choice.cs に合わせています。

sql
WITH [Masters] AS (
    SELECT
        [Row].[RowOrdinal],
        ordinal AS [ColOrdinal],
        VALUE   AS [ColValue]
    FROM
        (
            SELECT
                VALUE   AS [RowValue],
                ordinal AS [RowOrdinal]
            FROM
                [dbo].[Wikis]
                CROSS APPLY STRING_SPLIT([Body], CHAR (10), 1)
            WHERE
                [SiteId] = @SiteId
        ) AS [Row]
        CROSS APPLY STRING_SPLIT([RowValue], ',', 1)
)
SELECT
    MAX(
        CASE [Masters].[ColOrdinal]
            WHEN 1 THEN [Masters].[ColValue]
            END
    )          AS [Value],
    MAX(
        CASE [Masters].[ColOrdinal]
            WHEN 2 THEN [Masters].[ColValue]
            END
    )          AS [Text],
    MAX(
        CASE [Masters].[ColOrdinal]
            WHEN 3 THEN [Masters].[ColValue]
            END
    )          AS [TextMini],
    MAX(
        CASE [Masters].[ColOrdinal]
            WHEN 4 THEN [Masters].[ColValue]
            END
    )          AS [CssClass],
 MAX(
        CASE [Masters].[ColOrdinal]
            WHEN 5 THEN [Masters].[ColValue]
            END
    )          AS [Style]
FROM
    [Masters]
GROUP BY
    [Masters].[RowOrdinal]
sql
WITH "Masters" AS (
    SELECT r.ord AS "RowOrdinal", c.ord AS "ColOrdinal", c.value AS "ColValue"
    FROM "Wikis" w
    CROSS JOIN LATERAL unnest(string_to_array(w."Body", chr(10)))
        WITH ORDINALITY r(value, ord)
    CROSS JOIN LATERAL unnest(string_to_array(r.value, ','))
        WITH ORDINALITY c(value, ord)
    WHERE w."SiteId" = @SiteId
)
SELECT
    MAX(CASE "ColOrdinal" WHEN 1 THEN "ColValue" END) AS "Value",
    MAX(CASE "ColOrdinal" WHEN 2 THEN "ColValue" END) AS "Text",
    MAX(CASE "ColOrdinal" WHEN 3 THEN "ColValue" END) AS "TextMini",
    MAX(CASE "ColOrdinal" WHEN 4 THEN "ColValue" END) AS "CssClass",
    MAX(CASE "ColOrdinal" WHEN 5 THEN "ColValue" END) AS "Style"
FROM "Masters"
GROUP BY "RowOrdinal"
ORDER BY "RowOrdinal";
sql
WITH `Masters` AS (
    SELECT r.ord AS `RowOrdinal`, c.ord AS `ColOrdinal`, c.value AS `ColValue`
    FROM `Wikis` w
    CROSS JOIN JSON_TABLE(
        CONCAT('[', REPLACE(JSON_QUOTE(w.`Body`), CONCAT(CHAR(92), 'n'), '","'), ']'),
        '$[*]' COLUMNS (ord FOR ORDINALITY, value LONGTEXT PATH '$')
    ) r
    CROSS JOIN JSON_TABLE(
        CONCAT('[', REPLACE(JSON_QUOTE(r.value), ',', '","'), ']'),
        '$[*]' COLUMNS (ord FOR ORDINALITY, value LONGTEXT PATH '$')
    ) c
    WHERE w.`SiteId` = @SiteId
)
SELECT
    MAX(CASE `ColOrdinal` WHEN 1 THEN `ColValue` END) AS `Value`,
    MAX(CASE `ColOrdinal` WHEN 2 THEN `ColValue` END) AS `Text`,
    MAX(CASE `ColOrdinal` WHEN 3 THEN `ColValue` END) AS `TextMini`,
    MAX(CASE `ColOrdinal` WHEN 4 THEN `ColValue` END) AS `CssClass`,
    MAX(CASE `ColOrdinal` WHEN 5 THEN `ColValue` END) AS `Style`
FROM `Masters`
GROUP BY `RowOrdinal`
ORDER BY `RowOrdinal`;

@SiteId は変数定義するか、サイト ID に直接書き換えて使います。PostgreSQL は unnest と WITH ORDINALITY、MySQL は JSON_TABLE の FOR ORDINALITY で行・列の位置を保持します。この例は改行とカンマを単純に分割するため、値にエスケープしたカンマやバックスラッシュを含まないマスタを対象とします。

たとえば次の内容の Wiki は、Value・Text・TextMini・CssClass の列を持つ 5 行として取得できます。

csv
100,保留,保留,status-new
130,伝票作成,伝票作成,status-preparation
140,伝票確認,伝票確認,status-preparation
150,発注依頼,発注依頼,status-preparation
900,部材発注済,部材発注済,status-rejected

関連ページ ​

変更履歴

第7版通知設定と Wiki マスタの取得 SQL を3種類のDBMSに対応
第6版DB メンテナンスに行サイズの診断ページへのリンクを追加
第5版セッションの仕組みとデータベースのテーブル構成の解説を追加し、性能とスケールアウトの説明を修正
第4版「構築・運用」を 1.5.8.1 のソースで検証して修正
第3版元記事への言及を整理し、必要なコードをページに収録。検索機能に一覧の検索と絞り込みを追加
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「構築・運用」セクションの記事を追加