7

我正在使用 MySQL 5.7。我创建了一个带有 DATETIME 类型的虚拟列(未存储)的表,上面有一个索引。当我在处理它时,我注意到 order by 并没有返回所有数据(我期望在顶部的一些数据丢失了)。MAX 和 MIN 的结果也是错误的。我跑之后

ANALYZE TABLE 
CHECK TABLE
OPTIMIZE TABLE

那么结果是正确的。我猜索引数据有问题,所以我有几个问题:

  1. 何时以及为什么会发生这种情况?
  2. 有没有办法防止这种情况?
  3. 在我运行的 3 个命令中,哪个是正确的?

我担心将来会发生这种情况,但我不会注意到。

编辑

按照评论中的要求,我添加了表定义:

CREATE TABLE `items` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) unsigned DEFAULT NULL,
  `image` json DEFAULT NULL,
  `status` json DEFAULT NULL,
  `status_expired` tinyint(1) GENERATED ALWAYS AS (ifnull(json_contains(`status`,'true','$.expired'),false)) VIRTUAL COMMENT 'used for index: it checks if status contains expired=true',
  `lifetime` tinyint(4) NOT NULL,
  `expiration` datetime GENERATED ALWAYS AS ((`create_date` + interval `lifetime` day)) VIRTUAL,
  `last_update` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  KEY `expiration` (`status_expired`,`expiration`) USING BTREE,
  CONSTRAINT `ts_competition_item_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `ts_user_core` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1312459 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED

返回错误结果的查询:

SELECT * FROM items ORDER BY expiration DESC;
SELECT max(expiration),min(expiration) FROM items;

谢谢

4

1 回答 1

9

TLDR;

问题是您的数据来自通过索引实现的虚拟列。您正在执行的检查、优化、分析操作会强制同步索引并修复任何错误。从此以后,您将获得正确的结果。至少在索引再次不同步之前。

为什么会发生

许多问题是由您的桌子设计问题引起的。让我们开始吧。

`status_expired` tinyint(1) GENERATED ALWAYS AS (ifnull(json_contains(`status`,'true','$.expired'),false)) VIRTUAL

毫无疑问,这是为了克服不能直接JSON在 mysql 中索引列的事实。您已经创建了一个虚拟列并对其进行了索引。一切都很好,但是这一列只能包含两个值之一;truefalse。这意味着它的节奏很差。因此,mysql 不太可能将这个索引用于任何事情。

但是我们可以看到您在创建索引时已经将status_expired列与列组合在一起。expired也许是为了克服上面提到的这种糟糕的基数。可是等等...

`expiration` datetime GENERATED ALWAYS AS ((`create_date` + interval `lifetime` day)) VIRTUAL,

过期是另一个虚拟列。这有一些影响。

在生成的虚拟列上创建二级索引时,生成的列值会在索引的记录中具体化。如果索引是覆盖索引(包括查询检索到的所有列),则从索引结构中的物化值中检索生成的列值,而不是“即时”计算。

参考:https ://dev.mysql.com/doc/refman/5.7/en/create-table-secondary-indexes.html#json-column-indirect-index

这与

VIRTUAL:不存储列值,而是在读取行时,在任何 BEFORE 触发器之后立即评估。虚拟列不占用存储空间。

参考:https ://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html

我们基于正确的原则创建虚拟列,即不应存储对列的简单操作生成的值以避免冗余,但通过在其上创建索引,我们重新引入了冗余。

建议的修复

根据提供的信息,您似乎并不需要该status_expired列甚至该expired列。超过保质期的商品已过期!

CREATE TABLE `items` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) unsigned DEFAULT NULL,
  `image` json DEFAULT NULL,
  `status` json DEFAULT NULL,
  `expire_date` datetime GENERATED ALWAYS AS ((`create_date` + interval `lifetime` day)) VIRTUAL,
  `last_update` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  KEY `expiration` (`expired_date`) USING BTREE,
  CONSTRAINT `ts_competition_item_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `ts_user_core` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1312459 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED

当您需要找出哪些项目已过期时,只需将当前日期与上表中的 expired_date 列进行比较。此处的不同之处在于,您在创建记录时expired计算一次,而不是每个查询中的计算项。expiry_date

这使您的表格更整洁,查询可能更快

于 2017-01-26T11:36:59.690 回答