我们的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`)
);
“摘要”表,其中包含活动表中会话总数的每日汇总,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;
实际的摘要查询类似于以下内容,计算会话数和交易数,然后按广告系列和(可选)过滤器进行分组。
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键值的所有摘要条目。 (这就是我们现在正在做的事情) 如果在汇总过程中执行查询,则会产生缺少数据的结果,这会产生负面影响。
将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”,并直接使用此视图而不是表格。 摘要表包含几十万行,我被告知视图性能很差。
允许创建重复条目,并在摘要完成后删除旧条目。 提前删除它们也有类似的问题。
添加一个包含0表示NULL的代理列,并在UNIQUE KEY中使用该代理(实际上,如果所有列都是非NULL,我们可以使用PRIMARY KEY)。
这个解决方案似乎是合理的,除了上面的例子只是一个例子;实际的数据库包含六个汇总表,其中一个汇总表包含UNIQUE KEY中的四个可为空的列。有人担心开销太大。
您是否有更好的解决方法,表格结构,更新过程或MySQL最佳实践可以提供帮助?
编辑:澄清“无效的意思”
包含NULL列的摘要行中的数据被认为仅属于汇总报告中的单个“全部”行的意义,总结了那些数据点不存在或未知的项目。因此,在摘要表本身的上下文中,含义是“没有值已知的那些条目的总和”。另一方面,在关系表中,这些确实是NULL结果。
将它们放入摘要表中的唯一键的唯一原因是在重新计算摘要报告时允许自动更新(按ON DUPLICATE KEY UPDATE)。
可能更好的方式来描述它是通过具体的例子,其中一个汇总表按照受访者给出的商业地址的邮政编码前缀在地理上对结果进行分组。并非所有受访者都提供了业务地址,因此事务和地址表之间的关系非常正确。在此数据的摘要表中,为每个邮政编码前缀生成一行,其中包含该区域内的数据摘要。生成另一行以显示未知邮政编码前缀的数据摘要。
将其余数据表更改为具有明确的“THERE_IS_NO_ZIP_CODE”0值,并在ZipCodePrefix表中放置表示此值的特殊记录是不合适的 - 该关系确实为NULL。
答案 0 :(得分:4)
我觉得(2)中的某些内容确实是最好的选择 - 或者至少,如果你是从头开始的话。在SQL中,NULL表示未知。如果你想要一些其他的意思,你真的应该使用一个特殊的值,0肯定是一个不错的选择。
您应该在整个数据库中执行此操作,而不仅仅是这一个表。然后你不应该结束奇怪的特殊情况。事实上,你应该能够摆脱现有的很多(例如:目前,如果你想要没有过滤器的摘要行,你有特殊情况“过滤器为空”而不是正常情况“filter =?”。)
您还应该继续在引用表中创建“不存在”条目,以保持FK约束有效(并避免特殊情况)。
PS:没有主键的表不是关系表,应该真的避免使用。
嗯,在这种情况下,你真的需要重复密钥更新吗?如果你正在进行INSERT ... SELECT,那么你可能会这样做。但是,如果您的应用程序正在提供数据,请手动执行 - 执行更新(将zip = null
映射到zip is null
),检查已更改的行数(MySQL返回此值),如果0执行插入操作
答案 1 :(得分: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为零的条目。
答案 2 :(得分:0)
对于现代版本的MariaDB(以前称为MySQL),如果使用代理列路径#5,则可以使用插入重复键更新语句来完成upsert。添加MySQL生成的存储列或MariaDB持久虚拟列以在可空字段上应用唯一性约束间接地将无意义数据保留在数据库之外以换取一些膨胀。
e.g。
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,用PERSISTENT替换STORED,索引需要持久性。
答案 3 :(得分:0)
我已经晚了十多年,但是我觉得我的解决方案应该是这里的答案,因为我有这个完全相同的问题,并且对我有用。如果您知道要更新的内容,则可以在现有摘要查询之前手动进行更新,然后忽略现有查询中filter_id为null的所有情况,这样就不会再次将其作为记录插入。
以您的示例为例:
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`);