2009-08-19 109 views
11

我们的MySQL网站分析数据库包含一个汇总表,当导入新的活动时,汇总表会一天更新。我们使用ON DUPLICATE KEY UPDATE,以便汇总覆盖以前的计算,但因为汇总表的UNIQUE KEY中的一列是可选的FK并且包含NULL值,所以遇到困难。MySQL在重复密钥更新中使用唯一密钥中的空值列

这些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`) 
); 

A“摘要”表含有活性表,d那些含有一个事务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; 

实际汇总查询是类似于以下,计数会话和交易的数量,然后通过运动和(可选)过滤器分组。除了案件的总结,其中过滤器_id是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 
    GROUP BY `day`, `campaign_id`, `filter_id` 
ON DUPLICATE KEY UPDATE 
    `sessions` = VALUES(`sessions`) 
    , `transactions` = VALUES(`transactions`) 
; 

一切都很正常。在这些情况下,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)。
    这个解决方案似乎是合理的,只是上面的例子只是一个例子;实际的数据库包含六十张摘要表,其中一个摘要表包含UNIQUE KEY中的四个可为空的列。有些人担心开销太多。

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

编辑:为了澄清

包含NULL列的汇总行中的数据“零的意思是”被认为只有在这个意义上一起属于那个被一个“包罗万象”的总结报告行总结那些数据点不存在或未知的项目。因此,在汇总表本身的范围内,其含义是“没有任何值已知的那些条目的总和”。另一方面,在关系表中,这些确实是NULL结果。

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

也许更好的方式来描述它是由一个具体的例子,其中一个汇总表分组结果地理上由回应者给出的商业地址的邮政编码前缀。并非所有的受访者都提供了一个商业地址,所以事务和地址表之间的关系非常正确。在此数据的汇总表中,将为每个邮政编码前缀生成一行,其中包含该区域内的数据汇总。将生成一个附加行以显示未知邮政编码前缀的数据摘要。

将其余数据表更改为具有明确的“THERE_IS_NO_ZIP_CODE”0值,并在表示此值的ZipCodePrefix表中放置特殊记录是不恰当的 - 该关系确实为NULL。

回答

4

我认为沿着(2)的观点确实是最好的选择 - 或者至少,如果你是从头开始的话。在SQL中,NULL意味着未知。如果你想要其他的意义,你真的应该使用一个特殊的值,0肯定是一个好选择。

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

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

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

编辑1

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

+0

是,汇总表是相当不明确的关系表。它只是保存报告结果的便利容器。 我的声明说:“这些零点意在表示‘不存在,且所有此类案件是等价的’”,也许是误导性的。在包含规范化数据的关系表中,filter_id和其他可空关系在摘要表中作为唯一关键字的一部分提及,确实具有“未知”的含义,并且不是任何主键或唯一键的一部分。 见编辑,上面。 – ryandenki 2009-08-19 07:50:05

+0

没错。我们使用INSERT ... SELECT,使用那里的ON DUPLICATE KEY子句来更新整天的条目。实际上,两年前的第一个实现就像你所建议的那样 - 首先选择数据,执行一些额外的操作,然后发出单独的INSERTS,并用WHERE子句考虑IS NULL情况。 该方法的优点是,插入各行的锁比INSERT ... SELECT方法的锁要短。但是这些锁只在使用行复制的主机上,我们可以用一个SQL语句替换所有的应用程序端代码。 – ryandenki 2009-08-20 02:41:18

0

将DEFAULT NULL列更改为DEFAULT 0,这允许UNIQUE KEY一致地匹配。这具有消极的副作用,使查询的发展过度复杂化。它迫使我们使用大量的“CASE filter_id = 0 THEN NULL ELSE filter_id END”,并且由于所有其他表都具有实际的filter_id NULL,所以会导致难以加入。

创建它返回“CASE过滤器_id = 0 THEN ELSE NULL过滤器_id END”的图,并使用该视图,而不是直接的表。汇总表包含几十万行,并且我被告知视图性能非常差。

在MySQL 5.x中查看性能将会很好,因为该视图不会做任何事情,而是用null替换零。除非在视图中使用聚合/排序,否则大多数针对视图的查询都将被查询优化器重新编写为仅打击基础表。

和当然,因为它是一个FK,你必须创建一个条目参照的表为零的ID。

0

随着MariaDB的(以前的MySQL)的现代版本,upserts可以简单地与重复键更新语句插入,如果你用代理列路线#5去完成。添加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将STORED替换为PERSISTENT,索引需要持久性。

MySQL Generated Columns MariaDB Virtual Columns