【问题标题】:Timetravel view in MySQLMySQL中的时间旅行视图
【发布时间】:2013-09-14 02:54:31
【问题描述】:

我需要在 mysql 中实现价格的时间旅行视图。 底价表是这样的:

CREATE TABLE product_price (
  product_id INT(11) NOT NULL,
  date_valid TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  price DECIMAL(15,4) NOT NULL,
  PRIMARY KEY(product_id,date_valid)
);

这个想法是,随着时间的推移,我会选择我预先输入的正确有效价格。 也许这个概念以后会更清楚。 我需要创建一个视图,以便为每个 product_id 获得最新价格。 过了一会儿,我找到了满足我需要的 SELECT:

SELECT * FROM  (
  SELECT product_id,price FROM product_pric
    WHERE date_valid <= CURRENT_TIMESTAMP
    ORDER BY date_valid DESC
) xx GROUP BY product_id;

为了创建所需的视图,我知道我不能使用子选择,需要创建一个或多个中间视图。像这样:

CREATE VIEW v_product_price_time AS
  SELECT product_id,price FROM product_pric
    WHERE date_valid <= CURRENT_TIMESTAMP
    ORDER BY date_valid DESC
;


CREATE VIEW v_product_price AS
  SELECT * FROM v_product_price_time GROUP BY product_id;

然后我得到的与我写的原始查询不同。 例如,我只用两行填充表格:

INSERT INTO product_price (product_id,date_valid,price ) VALUES ( 1,'2013-01-01',41.40 );
INSERT INTO product_price (product_id,date_valid,price ) VALUES ( 1,'2013-01-03',42.0 );

原始查询返回正确的数据 (1,42.0),但查询视图没有。我总是得到 (1,41.40)。

我肯定遗漏了一些东西,因为我不太了解 MySQL。 使用另一个开源 RDBMS 我已经做过类似的事情,但现在我需要处理 MySQL v5.5 并且无法更改它。 但是开发者论坛中的文档和一些搜索并没有让我找到解决方案。 关于如何解决这个问题的任何想法? TIA。

【问题讨论】:

  • 您的表主键需要更改为 PRIMARY KEY(product_id,date_valid DESC)。从长远来看,您将获得更好的性能。
  • @Gilbert Le Blanc 你绝对是对的。但到目前为止,我很难理解。

标签: mysql view greatest-n-per-group


【解决方案1】:

无论是否在视图中,都使用此查询。

SELECT p1.* FROM (
  SELECT * FROM product_price
  WHERE date_valid <= CURRENT_TIMESTAMP
) p1
LEFT JOIN (
  SELECT product_id, date_valid FROM product_price
  WHERE date_valid <= CURRENT_TIMESTAMP
) p2
ON p1.product_id = p2.product_id AND p1.date_valid < p2.date_valid
WHERE p2.date_valid IS NULL

该查询创建了 2 个派生表,效率不高,而且阅读起来也有点困难。您可以尝试为此创建另一个视图:

CREATE VIEW product_price_past_dates AS (
  SELECT * FROM product_price
  WHERE date_valid <= CURRENT_TIMESTAMP
);

然后将原来的查询改写为:

SELECT p1.* FROM product_price_past_dates p1
LEFT JOIN product_price_past_dates p2
ON p1.product_id = p2.product_id AND p1.date_valid < p2.date_valid
WHERE p2.date_valid IS NULL

然后您可以在使用前一个视图的查询上创建视图:

CREATE VIEW v_product_price_time AS (
  SELECT p1.* FROM product_price_past_dates p1
  LEFT JOIN product_price_past_dates p2
  ON p1.product_id = p2.product_id AND p1.date_valid < p2.date_valid
  WHERE p2.date_valid IS NULL
);

并以最简单的查询结束:

SELECT * FROM v_product_price_time;

小提琴here.

为什么 GROUP BY 不起作用: 错误基本上在于 GROUP BY 子句的使用不当。经验法则(尽管不是 100% 正确)将始终在选择中使用与 GROUP BY 中相同的字段。否则,MySQL 将从 select 中而不是 GROUP BY 中的字段中选择任何值。

有关更多信息,您应该查看MySQL documentation。我觉得很清楚。

非常详细的解释:

SELECT * FROM  (
  SELECT product_id,price FROM product_pric
    WHERE date_valid <= CURRENT_TIMESTAMP
    ORDER BY date_valid DESC
) xx GROUP BY product_id;

0 syntactical errors
1 semantical error
1 warning
  1. 语义错误:选择 product_id 和 price 并仅按 product_id 分组将导致每个价格返回不可预测的价格。您不希望结果集中有不可预测的值,否则,您不会选择它。因此,您 100% 信任一个您无法预测的值。这确实是一个错误
  2. 警告:您正在对结果集进行排序,然后通过将其包装在 GROUP BY 中来消除该顺序。订购某物然后为其生成不同的订单是没有意义的。这会降低性能。

应使用 2 个基本的每个组最大 n 解决方案之一来修复先前的查询。我提供了最短的一个,即左连接。

CREATE VIEW v_product_price_time AS
  SELECT product_id,price FROM product_pric
    WHERE date_valid <= CURRENT_TIMESTAMP
    ORDER BY date_valid DESC
;

0 syntactical errors
0 semantical errors
0 warnings

完全有效的查询。根本没有cmets。

CREATE VIEW v_product_price AS
  SELECT * FROM v_product_price_time GROUP BY product_id;

0 syntactical errors
1 semantical errors
0 warnings
  1. 语义错误:再次选择 product_id 和 price 并且仅按 product_id 分组。与上述相同,这将导致不可预知的结果。

因此,您基本上是在比较 2 个不可预测的结果并期望结果相同。有趣的是,比较 2 个不可预测的结果比仅 1 个不可预测的结果更容易出错。因此,认为自己很幸运能够增加在代码中发现此错误的机会。问这个问题的人:How can I update the value of one field to the most often used value of another field?

当他发现查询没有像他预期的那样工作时,他会发现一个有趣的惊喜。此外,其中 4 个答案中有 3 个没有正确分组并返回不可预测的结果。更不用说 Bohemian 的评论,他在其中陈述了他的代码 will always work。所以,恭喜 Enzo,你刚刚除以 0 :)

希望这会有所帮助。

【讨论】:

  • 您的建议确实有效,即使我不明白“免费查询”和视图背后的那个之间的区别。
  • @Enzo 正如我在回答中所说,错误使用 GROUP BY 的结果基本上是不可预测的。不要查看查询中的逻辑,而是查看 GROUP BY。该查询恰好不需要 GROUP BY,因此删除它很容易解决它:) 无论如何,如果您将来需要 GROUP BY,您可以遵循经验法则。这将使它正常工作。顺便说一句,这只是产生这种复杂性的 MySQL 灵活性......您的查询在 ANSI SQL 中是不合法的
  • 您的查询仅使用一个 product_id,这不是我的情况。对不起,如果我没有说清楚。也就是说,到目前为止,我已将所有(不成功的)结果放在 this fiddle 中(感谢您让我知道它们的存在)。我担心第二个查询完全符合我的要求,无法翻译成语义上等价的 VIEW。问题在于排序。在(子)查询中,排序被保留,而在 VIEW 中则不是。那么解决方案是什么,如果有的话?我需要创建一个视图以便在任何后续查询中“隐藏”一个表。
  • @Enzo 样本数据让事情变得更清晰。总是添加足够的东西来进行测试:) 再次检查答案
  • @Enzo 看来您没有注意我提供给 MySQL 文档的链接。如果你阅读它,你会看到这个The server is free to choose any value from each group, so unless they are the same, the values chosen are indeterminate.。基本上,你是在抛硬币:你期待反面而你碰巧得到反面的事实只是巧合。通过编写适当的 SQL 代码来消除巧合。在答案中的链接中了解更多信息。
【解决方案2】:

我会这样做:

SELECT *
FROM product_price pp
INNER JOIN (
  SELECT product_id,MAX(date_valid) most_recent_date
  FROM product_price pp
  WHERE date_valid <= CURRENT_TIMESTAMP
  GROUP BY product_id
) aux ON aux.product_id = pp.product_id
AND aux.most_recent_date = pp.date_valid

在子查询中,您可以选择每个 product_id 的最新有效日期以及 product_id。这将为每个 product_id (其日期有效,等于或小于当前时间戳)提供一行。现在您有了 product_id 和 date_valid(您的 PK),您可以安全地将这些行与 product_price 表连接起来以获取其余数据。这个“技巧”并不总是作为在子查询中选择组中的一行的解决方案,但由于您需要获取每个产品的最新日期,因此您可以利用 MAX 函数。

如果需要,您可以将子查询放在视图中:

CREATE VIEW most_recent_product AS (
  SELECT product_id,MAX(date_valid) most_recent_date
  FROM product_price pp
  WHERE date_valid <= CURRENT_TIMESTAMP
  GROUP BY product_id
)

然后:

SELECT *
FROM product_price pp
INNER JOIN most_recent_product aux ON aux.product_id = pp.product_id
AND aux.most_recent_date = pp.date_valid

这是fiddle

【讨论】:

    猜你喜欢
    • 2023-02-20
    • 1970-01-01
    • 2021-09-01
    • 2015-12-30
    • 2022-01-08
    • 1970-01-01
    • 1970-01-01
    • 2017-07-31
    • 2014-10-29
    相关资源
    最近更新 更多