15

我们的 MySQL 网络分析数据库包含一个汇总表,该汇总表在导入新活动时全天更新。我们使用 ON DUPLICATE KEY UPDATE 以便汇总覆盖之前的计算,但由于汇总表的 UNIQUE KEY 中的列之一是可选的 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 的那些会话的总数。这些摘要是分开的,每个活动和(可选)过滤器的组合都有一个。这是一个使用 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. 添加一个包含 0 表示 NULL 的代理列,并在 UNIQUE KEY 中使用该代理项(实际上,如果所有列都不是 NULL,我们可以使用 PRIMARY KEY)。
    这个解决方案似乎是合理的,只是上面的例子只是一个例子;实际的数据库包含六个汇总表,其中一个在 UNIQUE KEY 中包含四个可为空的列。有些人担心开销太大。

您是否有更好的解决方法、表结构、更新过程或 MySQL 最佳实践可以提供帮助?

编辑:澄清“空的含义”

包含 NULL 列的汇总行中的数据仅在汇总报告中作为单个“包罗万象”行的意义上被认为属于一起,汇总了该数据点不存在或未知的那些项目。因此,在汇总表本身的上下文中,含义是“那些不知道值的条目的总和”。另一方面,在关系表中,这些确实是 NULL 结果。

将它们放入汇总表上的唯一键的唯一原因是在重新计算汇总报告时允许自动更新(通过 ON DUPLICATE KEY UPDATE)。

也许更好的描述方式是通过具体示例,其中一个汇总表按地理区域按受访者提供的公司地址的邮政编码前缀分组结果。并非所有受访者都提供了营业地址,因此事务和地址表之间的关系是非常正确的 NULL。在此数据的汇总表中,为每个邮政编码前缀生成一行,其中包含该区域内的数据汇总。将生成一个附加行来显示不知道邮政编码前缀的数据的摘要。

将其余数据表更改为具有显式“THERE_IS_NO_ZIP_CODE”0 值,并在 ZipCodePrefix 表中放置一个表示该值的特殊记录是不正确的——这种关系确实是 NULL。

4

4 回答 4

4

我认为(2)中的东西确实是最好的选择——或者,至少,如果你是从头开始的话。在 SQL 中,NULL 表示未知。如果你想要一些其他的含义,你真的应该为此使用一个特殊的值,而 0 肯定是一个不错的选择。

您应该在整个数据库中执行此操作,而不仅仅是这个表。那么你不应该以奇怪的特殊情况结​​束。实际上,您应该能够摆脱很多当前的(例如:目前,如果您想要没有过滤器的摘要行,那么您有特殊情况“过滤器为空”而不是正常情况“过滤器=?”。)

您还应该继续在所引用的表中创建一个“不存在”条目,以保持 FK 约束有效(并避免特殊情况)。

PS:没有主键的表不是关系表,应该避免。

编辑 1

嗯,在那种情况下,你真的需要重复键更新吗?如果您正在执行 INSERT ... SELECT,那么您可能会这样做。但是,如果您的应用程序正在提供数据,只需手动执行 — 执行更新(映射zip = nullzip is null),检查更改了多少行(MySQL 返回此数据),如果为 0,则执行插入。

于 2009-08-19T07:07:42.247 回答
1

使用现代版本的 MariaDB(以前的 MySQL),如果您使用代理列路由 #5,则可以简单地通过在重复键更新语句上插入来完成更新插入。添加 MySQL 生成的存储列或 MariaDB 持久虚拟列以对可空字段应用唯一性约束,从而间接地将无意义的数据保留在数据库之外,以换取一些膨胀。

例如

如果不存在则创建表栏 (
    id INT PRIMARY KEY AUTO_INCREMENT,
    datebin 日期不为空,
    baz1_id INT 默认 NULL,
    vbaz1_id INT AS (COALESCE(baz1_id, -1)) 已存储,
    baz2_id INT 默认 NULL,
    vbaz2_id INT AS (COALESCE(baz2_id, -1)) 已存储,
    Blam DOUBLE NOT NULL,
    唯一(日期,vbaz1_id,vbaz2_id)
);

INSERT INTO bar (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 中的视图性能会很好,因为视图除了用 null 替换零之外什么都不做。除非您在视图中使用聚合/排序,否则大多数针对视图的查询都将由查询优化器重新编写,以仅命中基础表。

当然,由于它是一个 FK,因此您必须在引用表中创建一个 id 为零的条目。

于 2009-08-19T07:15:30.547 回答
0

我迟到了十多年,但我觉得我的解决方案应该是这里的答案,因为我遇到了完全相同的问题,这对我有用。如果您知道要更新什么,您可以在现有汇总查询之前手动更新它们,然后忽略现有查询中 filter_id 为空的所有情况,这样它就不会再次作为记录插入。

对于您的示例:

UPDATE `Summary` s
    LEFT JOIN `Activity` a
    ON s.`campaign_id` = a.`campaign_id`
SET s.`sessions`     = a.COUNT(`session_id`)                ,
SET s.`transactions` = a.COUNT(`transaction_id` IS NOT NULL)
WHERE s.`day`         = a.`day`
AND   s.`campaign_id` = a.`campaign_id`
AND   s.`filter_id` IS NULL
AND   a.`filter_id` IS NULL;

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
    WHERE `filter_id` IS NOT NULL
    GROUP BY `day`, `campaign_id`, `filter_id`
ON DUPLICATE KEY UPDATE
    `sessions` = VALUES(`sessions`)
    , `transactions` = VALUES(`transactions`);
于 2020-10-15T22:22:31.813 回答