一意のキーでnullable列を使用した重複キーアップデートのmysql
-
18-09-2019 - |
質問
MySQL Web Analyticsデータベースには、新しいアクティビティがインポートされると1日を通して更新される要約テーブルが含まれています。要約が以前の計算を上書きするためには、重複キーアップデートで使用されますが、概要表の一意のキーの列の1つはオプションのFKであり、null値が含まれているため、困難があります。
これらのヌルは「存在しないことを意味することを意図しており、そのようなすべてのケースは同等です」。もちろん、MySQLは通常、ヌルを「不明を意味するものとして扱い、そのようなすべての場合は同等ではありません」。
基本構造は次のとおりです。
各セッションのエントリを含む「アクティビティ」テーブル、それぞれがキャンペーンに属し、いくつかのエントリにオプションのフィルターとトランザクション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キー値を含むすべての要約エントリを削除します。 (これは私たちが今やっていることです)これは、要約プロセス中にクエリが実行された場合、欠落データで結果を返すことの負の副作用を持っています。
デフォルトのnull列をデフォルト0に変更します。これにより、一意のキーを一貫して一致させることができます。これには、要約表に対するクエリの開発を過度に複雑にするという負の副作用があります。他のすべてのテーブルにはフィルター_IDの実際のヌルがあるため、多くの「ケースフィルター_ID= 0以降、null else filter_id end」を使用することを強制します。
「ケースFilter_Id = 0、次にnull else filter_id end」を返すビューを作成し、テーブルの代わりにこのビューを直接使用します。概要表には数十万行が含まれており、ビューパフォーマンスが非常に悪いと言われています。
重複したエントリを作成し、要約が完了した後に古いエントリを削除します。事前にそれらを削除するのに同様の問題があります。
nullに0を含む代理列を追加し、一意のキーでその代理を使用します(実際には、すべての列がnullでない場合はプライマリキーを使用できます)。
上記の例が例であることを除いて、このソリューションは合理的と思われます。実際のデータベースには、半ダースの概要テーブルが含まれており、そのうちの1つには一意のキーに4つのnullable列が含まれています。オーバーヘッドが多すぎるという懸念があります。
より良い回避策、テーブル構造、更新プロセス、またはMySQLのベストプラクティスが役立ちますか?
編集:「nullの意味」を明確にする
null列を含む概要行のデータは、要約レポートで単一の「キャッチオール」行であるという意味でのみ一緒に属すると見なされ、そのデータポイントが存在しないか、不明である項目を要約します。したがって、サマリーテーブル自体のコンテキスト内では、意味は「値が知られていないエントリの合計」です。一方、リレーショナルテーブル内では、これらは本当にヌルの結果です。
サマリーテーブルの一意のキーにそれらを配置する唯一の理由は、要約レポートを再計算するときに自動更新(重複するキーアップデートで)を可能にすることです。
たぶん、それを説明するより良い方法は、概要表の1つが回答者から与えられたビジネスアドレスの郵便番号のプレフィックスによって地理的に結果を得ることです。すべての回答者がビジネスアドレスを提供するわけではないため、トランザクションテーブルとアドレステーブルの関係は非常に正確にヌルです。このデータの概要表では、郵便番号のプレフィックスごとに行が生成され、その領域内のデータの概要が含まれています。追加の行が生成され、郵便番号のプレフィックスがわかっていないデータの概要を表示します。
残りのデータテーブルを変更して、明示的な「there_is_no_zip_code」0-valueを持ち、この値を表すzipcodeprefixテーブルに特別なレコードを配置することは不適切です。関係は本当にnullです。
解決
(2)の線に沿った何かが本当に最善の策だと思います - または、少なくとも、あなたがゼロから始めていた場合だと思います。 SQLでは、nullは不明です。他の意味が必要な場合は、そのために特別な価値を実際に使用する必要があります。0は確かにOK選択です。
これを行う必要があります 全体 このテーブルだけでなく、データベース。そうすれば、奇妙な特別なケースで終わらないでください。実際、現在のものの多くを取り除くことができるはずです(例:現在、フィルターがない要約行が必要な場合は、通常のケースとは対照的に特別なケース「フィルターはヌル」があります。 "フィルター=?"。)
また、FK制約を有効に保つために、紹介されたテーブルにも「存在しない」エントリも作成します(特別なケースを避けます)。
PS:主要なキーを備えた表は、リレーショナルテーブルではなく、実際には避ける必要があります。
編集1
うーん、その場合、実際に重複したキーアップデートが必要ですか?挿入を行っている場合は...選択してください。ただし、アプリがデータを提供している場合は、手作業で行うだけです - 更新を行います(マッピング zip = null
に zip is null
)、挿入物を実行する場合、行が変更された行の変更(mysqlがこれを返します)を確認します。
他のヒント
デフォルトのnull列をデフォルト0に変更します。これにより、一意のキーを一貫して一致させることができます。これには、要約表に対するクエリの開発を過度に複雑にするという負の副作用があります。他のすべてのテーブルにはフィルター_IDの実際のヌルがあるため、多くの「ケースフィルター_ID= 0以降、null else filter_id end」を使用することを強制します。
「ケースFilter_Id = 0、次にnull else filter_id end」を返すビューを作成し、テーブルの代わりにこのビューを直接使用します。概要表には数十万行が含まれており、ビューパフォーマンスが非常に悪いと言われています。
ビューはゼロをnullに置き換える以外に何もしないので、mysql 5.xのビューパフォーマンスは問題ありません。ビューで集約/ソートを使用しない限り、ビューに対するほとんどのクエリは、クエリオプティマイザーによって書き直され、基礎となるテーブルを押します。
そしてもちろん、FKなので、ゼロのIDを持つ紹介されたテーブルにエントリを作成する必要があります。
MariadB(以前のMySQL)の最新バージョンでは、Surrogate Column Route#5を使用すると、重複するキーアップデートステートメントに挿入されてUpsertsを単純に実行できます。 MySQLの生成された保存された列またはMariadB永続的な仮想列を追加して、ヌル可能なフィールドに一意性制約を適用すると、膨張と引き換えにナンセンスデータがデータベースからナンセンスデータを排除します。
例えば
CREATE TABLE IF NOT EXISTS bar ( id INT PRIMARY KEY AUTO_INCREMENT, datebin DATE NOT NULL, baz1_id INT DEFAULT NULL, vbaz1_id INT AS (COALESCE(baz1_id, -1)) STORED, baz2_id INT DEFAULT NULL, vbaz2_id INT AS (COALESCE(baz2_id, -1)) STORED, blam DOUBLE NOT NULL, UNIQUE(datebin, vbaz1_id, vbaz2_id) ); INSERT INTO bar (datebin, baz1_id, baz2_id, blam) VALUES ('2016-06-01', null, null, 777) ON DUPLICATE KEY UPDATE blam = VALUES(blam);
保存されているMariadb交換の場合、インデックスには永続性が必要です。