15

当社の 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」です。

回避策のアイデアや、これまでに思いついたものに対するフィードバックを探しています。これまでに考えた回避策は次のとおりです。

  1. 要約を実行する前に、NULL キー値を含むすべての要約エントリを削除します。(これが現在行っていることです) これには、集計プロセス中にクエリが実行されると、データが欠落している結果が返されるというマイナスの副作用があります。

  2. DEFAULT NULL 列を DEFAULT 0 に変更します。これにより、UNIQUE KEY を一貫して一致させることができます。これには、サマリー テーブルに対するクエリの開発が過度に複雑になるというマイナスの影響があります。これにより、多くの「CASE filter_id = 0 THEN NULL ELSE filter_id END」を使用せざるを得なくなり、他のすべてのテーブルで実際には filter_id が NULL になっているため、結合が困難になります。

  3. 「CASE filter_id = 0 THEN NULL ELSE filter_id END」を返すビューを作成し、テーブルの代わりにこのビューを直接使用します。集計テーブルには数十万行が含まれており、ビューのパフォーマンスが非常に悪いと言われています。

  4. 重複エントリの作成を許可し、要約が完了したら古いエントリを削除します。事前に削除するのと同様の問題があります。

  5. 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 です。

4

4 に答える 4

4

(2)に沿った何かが本当に最善の策だと思います—または、少なくとも、ゼロから始めた場合はそうなります. SQL では、NULL は不明を意味します。他の意味が必要な場合は、そのために特別な値を使用する必要があり、0 を選択しても問題ありません。

この 1 つのテーブルだけでなく、データベース全体でこれを行う必要があります。次に、奇妙な特殊なケースに巻き込まれることはありません。実際、現在のものの多くを取り除くことができるはずです (例: 現在、フィルターのない集計行が必要な場合は、通常のケースとは対照的に、「フィルターが null である」という特殊なケースがあります。 "フィルター = ?".)

また、FK 制約を有効に保つ (そして特殊なケースを避ける) ために、参照先のテーブルにも "存在しない" エントリを作成する必要があります。

PS: 主キーを持つテーブルはリレーショナル テーブルではないため、避ける必要があります。

編集 1

うーん、その場合、実際に on duplicate key update が必要ですか? INSERT ... SELECT を実行している場合は、おそらく実行します。ただし、アプリがデータを提供している場合は、手動で行います — 更新 ( へのマッピング)zip = nullzip is null行い、変更された行数を確認します (MySQL はこれを返します)。0 の場合は挿入を行います。

于 2009-08-19T07:07:42.247 に答える
1

最近のバージョンの MariaDB (以前の MySQL) では、サロゲート カラム ルート #5 を使用すると、重複キー更新ステートメントに挿入するだけでアップサートを実行できます。MySQL の生成された格納された列または MariaDB の永続的な仮想列を追加して、null 許容フィールドに一意性制約を適用すると、データベースから意味のないデータが間接的に除外され、肥大化と引き換えになります。

例えば

CREATE TABLE IF NOT EXISTS バー (
    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(日付ビン、vbaz1_id、vbaz2_id)
);

INSERT INTO バー (datebin、baz1_id、baz2_id、blam)
    値 ('2016-06-01'、ヌル、ヌル、777)
重複キーの更新について
    非難 = 値 (非難);

MariaDB の場合、STORED を PERSISTENT に置き換えます。インデックスには永続性が必要です。

MySQL で生成された列 MariaDB 仮想列

于 2016-06-22T11:37:01.483 に答える
0

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」を返すビューを作成し、テーブルの代わりにこのビューを直接使用します。集計テーブルには数十万行が含まれており、ビューのパフォーマンスが非常に悪いと言われています。

ビューはゼロをヌルに置き換えるだけなので、MySQL 5.x でのビューのパフォーマンスは問題ありません。ビューで集計/並べ替えを使用しない限り、ビューに対するほとんどのクエリは、クエリ オプティマイザーによって書き直され、基になるテーブルにヒットするだけになります。

そしてもちろん、これは FK であるため、参照先のテーブルに ID ゼロのエントリを作成する必要があります。

于 2009-08-19T07:15:30.547 に答える