当社の MySQL Web 分析データベースには、新しいアクティビティがインポートされるたびに更新される概要テーブルが含まれています。要約が以前の計算を上書きするために ON DUPLICATE KEY UPDATE を使用しますが、要約テーブルの UNIQUE KEY の列の 1 つがオプションの FK であり、NULL 値が含まれているため、問題が発生しています。
これらの NULL は、「存在せず、そのような場合はすべて同等である」ことを意味することを意図しています。もちろん、MySQL は通常、NULL を「不明であり、そのようなすべてのケースは同等ではない」という意味として扱います。
基本的な構造は次のとおりです。
各セッションのエントリを含む「アクティビティ」テーブル。それぞれがキャンペーンに属し、いくつかのエントリのオプションのフィルターとトランザクション ID を含みます。
CREATE TABLE `Activity` (
`session_id` INTEGER AUTO_INCREMENT
, `campaign_id` INTEGER NOT NULL
, `filter_id` INTEGER DEFAULT NULL
, `transaction_id` INTEGER DEFAULT NULL
, PRIMARY KEY (`session_id`)
);
アクティビティ テーブル内の合計セッション数の毎日のロールアップと、トランザクション ID を含むセッションの合計数を含む「概要」テーブル。これらの概要は分割され、キャンペーンと (オプションの) フィルターの組み合わせごとに 1 つずつ表示されます。これは、MyISAM を使用した非トランザクション テーブルです。
CREATE TABLE `Summary` (
`day` DATE NOT NULL
, `campaign_id` INTEGER NOT NULL
, `filter_id` INTEGER DEFAULT NULL
, `sessions` INTEGER UNSIGNED DEFAULT NULL
, `transactions` INTEGER UNSIGNED DEFAULT NULL
, UNIQUE KEY (`day`, `campaign_id`, `filter_id`)
) ENGINE=MyISAM;
実際の集計クエリは次のようなもので、セッションとトランザクションの数をカウントアップし、キャンペーンと (オプション) フィルターでグループ化します。
INSERT INTO `Summary`
(`day`, `campaign_id`, `filter_id`, `sessions`, `transactions`)
SELECT `day`, `campaign_id`, `filter_id
, COUNT(`session_id`) AS `sessions`
, COUNT(`transaction_id` IS NOT NULL) AS `transactions`
FROM Activity
GROUP BY `day`, `campaign_id`, `filter_id`
ON DUPLICATE KEY UPDATE
`sessions` = VALUES(`sessions`)
, `transactions` = VALUES(`transactions`)
;
filter_id が NULL の場合の要約を除いて、すべてうまく機能します。このような場合、ON DUPLICATE KEY UPDATE 句は既存の行と一致せず、毎回新しい行が書き込まれます。これは、「NULL != NULL」であるためです。ただし、一意のキーを比較するときに必要なのは「NULL = NULL」です。
回避策のアイデアや、これまでに思いついたものに対するフィードバックを探しています。これまでに考えた回避策は次のとおりです。
要約を実行する前に、NULL キー値を含むすべての要約エントリを削除します。(これが現在行っていることです) これには、集計プロセス中にクエリが実行されると、データが欠落している結果が返されるというマイナスの副作用があります。
DEFAULT NULL 列を DEFAULT 0 に変更します。これにより、UNIQUE KEY を一貫して一致させることができます。これには、サマリー テーブルに対するクエリの開発が過度に複雑になるというマイナスの影響があります。これにより、多くの「CASE filter_id = 0 THEN NULL ELSE filter_id END」を使用せざるを得なくなり、他のすべてのテーブルで実際には filter_id が NULL になっているため、結合が困難になります。
「CASE filter_id = 0 THEN NULL ELSE filter_id END」を返すビューを作成し、テーブルの代わりにこのビューを直接使用します。集計テーブルには数十万行が含まれており、ビューのパフォーマンスが非常に悪いと言われています。
重複エントリの作成を許可し、要約が完了したら古いエントリを削除します。事前に削除するのと同様の問題があります。
NULL に 0 を含むサロゲート列を追加し、UNIQUE KEY でそのサロゲートを使用します (実際には、すべての列が NULL でない場合は PRIMARY KEY を使用できます)。
上記の例が単なる例であることを除けば、この解決策は妥当に思えます。実際のデータベースには半ダースのサマリー テーブルが含まれており、そのうちの 1 つには UNIQUE KEY に 4 つの null 値を許容する列が含まれています。オーバーヘッドが大きすぎるという懸念もあります。
役立つ回避策、テーブル構造、更新プロセス、または MySQL のベスト プラクティスはありますか?
編集:「nullの意味」を明確にする
NULL 列を含む要約行のデータは、そのデータ ポイントが存在しないか不明なアイテムを要約した、要約レポートの単一の「キャッチオール」行であるという意味でのみ、一緒に属していると見なされます。したがって、要約テーブル自体のコンテキスト内では、意味は「値が不明なエントリの合計」です。一方、リレーショナル テーブル内では、これらは実際には NULL の結果です。
それらをサマリー テーブルの一意のキーに配置する唯一の理由は、サマリー レポートを再計算するときに (ON DUPLICATE KEY UPDATE による) 自動更新を可能にするためです。
要約テーブルの 1 つが、回答者が指定した会社の住所の郵便番号プレフィックスによって地理的に結果をグループ化する具体的な例を使用すると、より適切に説明できます。すべての回答者が会社の住所を提供しているわけではないため、トランザクション テーブルと住所テーブルの関係はまったく正しく NULL です。このデータの要約テーブルでは、各郵便番号プレフィックスに対して行が生成され、その地域内のデータの要約が含まれます。追加の行が生成され、郵便番号のプレフィックスが不明なデータの概要が表示されます。
残りのデータ テーブルを変更して明示的な "THERE_IS_NO_ZIP_CODE" 0 値を持たせ、この値を表す ZipCodePrefix テーブルに特別なレコードを配置することは不適切です。その関係は本当に NULL です。