MySQL在值更改时插入新行

时间:2011-08-22 04:15:01

标签: mysql sql perl sql-update

对于我正在进行的个人项目,我想在Steam,Impulse,EA Origins和其他几个网站上制作游戏价格线图。目前,我已经修改了SteamCalculator.com使用的脚本,以记录每个国家/地区代码或每个网站中每个游戏的当前价格(如果适用的话,销售价格)。我还有一个列存储价格的日期。我目前的表格看起来像这样:

THIS STRUCTURE IS NO LONGER VALID. SEE BELOW
+----------+------+------+------+------+------+------+------------+
| steam_id |  us  |  at  |  au  |  de  |  no  |  uk  |    date    |
+----------+------+------+------+------+------+------+------------+
|  112233  |  999 |  899 |  999 | NULL |  899 |  699 |  2011-8-21 |
|  123456  | 1999 |  999 | 1999 |  999 |  999 |  999 |  2011-8-20 |
|    ...   |  ... |  ... |  ... |  ... |  ... |  ... |     ...    |
+----------+------+------+------+------+------+------+------------+

目前每个国家/地区都是单独更新的(有一个for循环通过这些国家/地区),但如果它会简化它,那么可以修改它以暂时将新价格存储到数组中,然后一次更新整行。无论如何,出于性能原因,我最终可能会这样做。

现在我的问题是确定如果其中一个价格发生变化,如何最好地更新此表。例如,我们假设2011年8月22日游戏112233在美国以4.99美元的价格开售,奥地利以3.99欧元的价格开售,其他价格保持不变。我需要表格看起来像这样:

THIS STRUCTURE IS NO LONGER VALID. SEE BELOW
+----------+------+------+------+------+------+------+------------+
| steam_id |  us  |  at  |  au  |  de  |  no  |  uk  |    date    |
+----------+------+------+------+------+------+------+------------+
|  112233  |  999 |  899 |  999 | NULL |  899 |  699 |  2011-8-21 |
|  123456  | 1999 |  999 | 1999 |  999 |  999 |  999 |  2011-8-20 |
|    ...   |  ... |  ... |  ... |  ... |  ... |  ... |     ...    |
|  112233  |  499 |  399 |  999 | NULL |  899 |  699 |  2011-8-22 |
+----------+------+------+------+------+------+------+------------+

我不想创建新行每次时间检查价格,否则我最终会在日复一日地拥有数百万行重复价格。我也不想像每次更改价格那样创建一个新行:

THIS STRUCTURE IS NO LONGER VALID. SEE BELOW
+----------+------+------+------+------+------+------+------------+
| steam_id |  us  |  at  |  au  |  de  |  no  |  uk  |    date    |
+----------+------+------+------+------+------+------+------------+
|  112233  |  999 |  899 |  999 | NULL |  899 |  699 |  2011-8-21 |
|  123456  | 1999 |  999 | 1999 |  999 |  999 |  999 |  2011-8-20 |
|    ...   |  ... |  ... |  ... |  ... |  ... |  ... |     ...    |
|  112233  |  499 |  899 |  999 | NULL |  899 |  699 |  2011-8-22 |
|  112233  |  499 |  399 |  999 | NULL |  899 |  699 |  2011-8-22 |
+----------+------+------+------+------+------+------+------------+

我可以通过使每个(steam_id, <country>)成为唯一索引然后将ON DUPLICATE KEY UPDATE添加到每个数据库查询来防止第一个问题而不是第二个问题。如果价格不同,这只会添加一行,但是会为每个更改的国家/地区添加新行。它也不允许两个不同日期的单个游戏的价格相同(例如,假设游戏112233稍后销售并返回9.99美元),所以这显然是一个糟糕的选择。

我可以通过将(steam_id, date)作为唯一索引然后将ON DUPLICATE KEY UPDATE添加到每个查询来防止第二个问题而不是第一个问题。运行脚本的每一天日期都已更改,因此它将创建一个新行。这种方法最终会产生数百条相同价格的日常生产线。

如果(并且仅当)自最近日期以来任何价格发生变化,我如何告诉MySQL创建新行?

更新 -

根据此线程中人员的建议,我已更改了数据库的架构,以便将来添加新的国家/地区代码,并避免需要一次更新整个行的问题。新架构看起来像:

+----------+------+---------+------------+
| steam_id |  cc  |  price  |    date    |
+----------+------+---------+------------+
|  112233  |  us  |   999   |  2011-8-21 |
|  123456  |  uk  |   699   |  2011-8-20 |
|    ...   |  ... |   ...   |     ...    |
+----------+------+---------+------------+

除了这个新架构之外,我发现我可以使用以下SQL查询来获取最新更新的价格:

SELECT `price` FROM `steam_prices` WHERE `steam_id` = 112233 AND `cc`='us' ORDER BY `date` ASC LIMIT 1

此时我的问题归结为:

是否可以(仅使用SQL而不是应用程序逻辑)仅在条件为真时插入行?例如:

INSERT INTO `steam_prices` (...) VALUES (...) IF price<>(SELECT `price` FROM `steam_prices` WHERE `steam_id` = 112233 AND `cc`='us' ORDER BY `date` ASC LIMIT 1)

the MySQL manual我找不到任何办法。我发现如果唯一索引相同,您可以忽略或更新。但是,如果我将价格作为一个独特的索引(允许我更新日期,如果它是相同的),那么我将无法识别游戏何时开始销售,然后返回其原始价格。例如:

+----------+------+---------+------------+
| steam_id |  cc  |  price  |    date    |
+----------+------+---------+------------+
|  112233  |  us  |   999   |  2011-8-20 |
|  112233  |  us  |   499   |  2011-8-21 |
|  112233  |  us  |   999   |  2011-8-22 |
|    ...   |  ... |   ...   |     ...    |
+----------+------+---------+------------+

此外,在找到并阅读MySQL Conditional INSERT之后,我创建并尝试了以下查询:

INSERT INTO `steam_prices`(
    `steam_id`,
    `cc`,
    `update`,
    `price`
)
SELECT '7870', 'us', NOW(), 999
FROM `steam_prices`
WHERE
    `price`<>999
    AND `update` IN (
        SELECT `update`
        FROM `steam_prices`
        ORDER BY `update`
        ASC LIMIT 1
    )

想法是插入行'7870', 'us', NOW(), 999 if(且仅当)最新price的{​​{1}}不是999.当我运行此时出现以下错误:

  

1235 - 此版本的MySQL尚不支持'LIMIT&amp; IN / ALL / ANY / SOME子查询'

有什么想法吗?

3 个答案:

答案 0 :(得分:6)

如果您只是将架构更改为以下内容,您可能会发现这更容易:

steam_id      integer
country       varchar(2)
date          date
price         float
primary key   (steam_id,country,date)

(与其他适当的指标一起),然后只是依次担心每个国家。

换句话说,您的for循环具有唯一的ID /国家/地区组合,因此它可以简单地查询该组合的最新日期记录,如果不同则添加新行。

这将使您的选择更加复杂,但我相信这是一个更好的解决方案,尤其是如果有任何机会可以在未来添加更多国家(它不会破坏架构在那种情况下)。

答案 1 :(得分:2)

首先,我建议您将数据存储在每个国家/地区硬编码较少的表单中:

+----------+--------------+------------+-------+
| steam_id | country_code | date       | price |
+----------+--------------+------------+-------+
|   112233 | us           | 2011-08-20 | 12.45 |
|   112233 | uk           | 2011-08-20 | 12.46 |
|   112233 | de           | 2011-08-20 | 12.47 |
|   112233 | at           | 2011-08-20 | 12.48 |
|   112233 | us           | 2011-08-21 | 12.49 |
|   ...... | ..           | .......... | ..... |
+----------+--------------+------------+-------+

从此处,您可以在前三列上放置一个主键...

现在提出关于不创建额外行的问题......这就是简单的事务+应用程序逻辑的优点。

  1. 开始交易
  2. 运行一个选择以查看相关记录是否存在
  3. 如果没有,请插入一个
  4. 这种方法有问题吗?

    希望这有帮助。

答案 2 :(得分:1)

经过实验,并在MySQL Conditional INSERThttp://www.artfulsoftware.com/infotree/queries.php#101的帮助下,我发现了一个有效的查询:

INSERT INTO `steam_prices`( 
    `steam_id`, 
    `cc`, 
    `price`,
    `update` 
) 
SELECT 7870, 'us', 999, NOW() 
FROM `steam_prices` AS p1
LEFT JOIN `steam_prices` AS p2 ON p1.`steam_id`=p2.`steam_id` AND p1.`update` < p2.`update`
WHERE 
    p2.`steam_id` IS NULL
    AND p1.`steam_id`=7870
    AND p1.`cc`='us'
    AND (
        p1.`price`<>999
    )

答案是首先返回没有早期时间戳的所有行。这是通过组内聚合完成的。只在时间戳较早的行上加入一个表。如果它无法加入(时间戳不早),那么您知道该行包含最新的时间戳。这些行在连接表中将具有 NULL id(无法加入)。

选择具有最新时间戳的所有行后,只抓取steam_id为您正在寻找的steam_id的行以及价格与您输入的新价格不同的行。如果此时该游戏的行没有不同的行,则自上次更新后价格没有变化,因此返回空集。返回空集时,SELECT语句将失败,并且不会插入任何内容。如果SELECT语句成功(找到了不同的价格),那么它将返回插入到表中的行7870, 'us', 999, NOW()

编辑 - 我实际上在一段时间后发现了上述查询的错误,之后我修改了它。如果自上次更新后价格发生了变化,上面的查询将插入一个新行,但如果该项目的数据库中当前没有价格,则不会插入一行。

要解决这个问题,我必须利用DUAL表(总是包含一行),然后在where子句中使用OR来测试不同的价格 OR 空集

INSERT INTO `steam_prices`( 
    `steam_id`, 
    `cc`, 
    `price`,
    `update` 
) 
SELECT 12345, 'us', 999, NOW() 
FROM DUAL
WHERE
    NOT EXISTS (
        SELECT `steam_id`
        FROM `steam_prices`
        WHERE `steam_id`=12345
    )
    OR
    EXISTS (
        SELECT p1.`steam_id`
        FROM `steam_prices` AS p1 
        LEFT JOIN `steam_prices` AS p2 ON p1.`steam_id`=p2.`steam_id` AND p1.`update` < p2.`update`
        WHERE 
            p2.`steam_id` IS NULL 
            AND p1.`steam_id`=12345 
            AND p1.`cc`='us' 
            AND ( 
                p1.`price`<>999
            )
    )

它很长,非常难看,而且非常复杂。但它完全像宣传的那样工作。如果某个steam_id数据库中没有价格,那么它会插入一个新行。如果已有价格,那么它会使用最新更新检查价格,如果不同,则会插入新行。