我们的 MySQL 网络分析数据库包含一个汇总表,该汇总表会随着新事件的导入全天更新。我们使用 ON DUPLICATE KEY UPDATE 以便汇总覆盖先前的计算,但由于汇总表的 UNIQUE KEY 中的列之一是可选的 FK,并且包含 NULL 值,因此存在困难。
这些 NULL 旨在表示“不存在,并且所有此类情况都是等效的”。当然,MySQL 通常将 NULL 视为“未知的,所有这些情况都不是等价的”。
基本结构如下:
一个“事件”表,包含每个 session 的条目,每个 session 都属于一个事件,以及一些条目的可选过滤器和事务 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`)
);
“摘要”表包含事件表中 session 总数的每日汇总,以及包含事务 ID 的 session 总数。这些摘要是分开的,每个事件和(可选)过滤器的组合都有一个。这是一个使用 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;
实际的汇总查询类似于以下内容,计算 session 和交易的数量,然后按事件和(可选)过滤器分组。
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”。
我正在寻找解决方法的想法或对我们迄今为止提出的建议的反馈。到目前为止我们想到的解决方法如下。
如果在汇总过程中执行查询,这会产生返回带有缺失数据的结果的负面影响。
这具有使针对汇总表的查询开发过于复杂的负面影响。它迫使我们使用大量的“CASE filter_id = 0 THEN NULL ELSE filter_id END”,并且由于所有其他表的 filter_id 都有实际的 NULL 值,因此连接起来很尴尬。
汇总表包含几十万行,我被告知 View 性能很差。
与提前删除它们有类似的问题。
这个方案看起来很合理,只不过上面的例子只是一个例子;实际数据库包含六个汇总表,其中一个包含 UNIQUE KEY 中的四个可为空的列。有些人担心开销太大。
您是否有更好的解决方法、表结构、更新过程或 MySQL 最佳实践可以提供帮助?
编辑:澄清“空的含义”
包含 NULL 列的汇总行中的数据仅在作为汇总报告中的单个“包罗万象”行的意义上被认为属于一起,汇总了那些数据点不存在或未知的项目。因此,在汇总表本身的上下文中,其含义是“未知值的那些条目的总和”。另一方面,在关系表中,这些确实是 NULL 结果。
将它们放入汇总表上的唯一键的唯一原因是在重新计算汇总报告时允许自动更新(通过 ON DUPLICATE KEY UPDATE)。
也许更好的描述方式是通过特定示例,其中一个汇总表按受访者提供的业务地址的邮政编码前缀对结果进行地理分组。并非所有受访者都提供公司地址,因此事务和地址表之间的关系非常正确为 NULL。在此数据的汇总表中,为每个邮政编码前缀生成一行,其中包含该区域内的数据汇总。生成额外的一行以显示不知道邮政编码前缀的数据摘要。
将其余数据表更改为具有明确的“THERE_IS_NO_ZIP_CODE”0 值,并在 ZipCodePrefix 表中放置一个表示该值的特殊记录是不正确的——这种关系确实是 NULL。
最佳答案
我认为 (2) 中的内容确实是最好的选择——或者,至少,如果你从头开始的话。在 SQL 中,NULL 表示未知。如果你想要一些其他的含义,你真的应该为此使用一个特殊的值,0当然是一个不错的选择。
您应该在整个数据库中执行此操作,而不仅仅是这个表。那么你不应该以奇怪的特殊情况结束。事实上,你应该能够摆脱很多你当前的(例如:目前,如果你想要没有过滤器的汇总行,你有特殊情况“过滤器为空”而不是正常情况“过滤器=?”。)
您还应该继续在引用表中创建一个“不存在”条目,以保持 FK 约束有效(并避免特殊情况)。
PS:没有主键的表不是关系表,应该真正避免。
编辑 1
嗯,在这种情况下,您真的需要重复 key 更新吗?如果您正在执行 INSERT ... SELECT,那么您可能会这样做。但是,如果您的应用程序正在提供数据,只需手动执行 — 执行更新(将 zip = null
映射到 zip is null
),检查更改了多少行(MySQL 返回此值),如果为 0,则执行插入操作。
关于MySQL ON DUPLICATE KEY UPDATE 在唯一键中具有可为空的列,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1298105/