【问题标题】:MySQL ON DUPLICATE KEY UPDATE with nullable column in unique keyMySQL ON DUPLICATE KEY UPDATE 具有唯一键中的可为空列
【发布时间】:2010-11-20 20:25:43
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: mysql nullable summarization


    【解决方案1】:

    我迟到了十多年,但我觉得我的解决方案应该是这里的答案,因为我遇到了完全相同的问题,这对我有用。如果您知道要更新什么,您可以在现有汇总查询之前手动更新它们,然后忽略现有查询中 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`);
    

    【讨论】:

      【解决方案2】:

      使用现代版本的 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 Generated Columns MariaDB Virtual Columns

      【讨论】:

        【解决方案3】:

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

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

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

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

        编辑 1

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

        【讨论】:

        • 是的,汇总表显然不是关系表。它只是一个保存报告结果的方便容器。我所说的“这些 NULL 旨在表示‘不存在,所有这些情况都是等价的’”,这可能具有误导性。在包含规范化数据的关系表中,我提到的 filter_id 和其他可以为空的关系作为汇总表中唯一键的一部分确实具有“未知”的含义,而不是任何主键或唯一键的一部分。见上面的编辑。
        • 完全正确。我们使用 INSERT...SELECT,使用 ON DUPLICATE KEY 子句全天更新条目。实际上,两年前的第一个实现正如您所建议的那样 - 首先选择数据,执行一些额外的操作,然后发出单独的 INSERTS,其中 WHERE 子句考虑了 IS NULL 情况。这种方法的优点是插入单个行的锁比 INSERT...SELECT 方法短。但是这些锁只在使用行复制的主控上,我们可以用一条 SQL 语句替换所有应用端代码。
        【解决方案4】:

        将 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 为零的条目。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-01-13
          • 1970-01-01
          • 2017-04-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多