MySQL вставляет новую строку при изменении значения - PullRequest
2 голосов
/ 22 августа 2011

Для личного проекта, над которым я сейчас работаю, я хочу составить линейный график цен на игры в 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, проходящий через страны), хотя, если это упростит ее, ее можно изменить, чтобы временно сохранить новые цены в массиве, а затем обновлять всю строку за раз. Скорее всего, в конечном итоге я буду делать это по соображениям производительности.

Теперь моя проблема - определить, как лучше всего обновить эту таблицу, если одна из цен изменится. Например, предположим, что 22.08.2011 игра 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)

С руководство по MySQL Не могу найти способ сделать это. Я обнаружил, что вы можете игнорировать или обновить, если уникальный индекс совпадает. Однако, если бы я сделал цену уникальным индексом (что позволило бы мне обновить дату, если она была такой же), я бы не смог распознать, когда игра поступила в продажу, а затем вернулся к своей первоначальной цене. Например:

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

Кроме того, после того, как я только нашел и прочитал Условная вставка MySQL , я создал и попробовал следующий запрос:

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, если (и только если) price самого последнего update не было 999. Когда я запустил это, я получил следующую ошибку:

1235 - Эта версия MySQL еще не поддерживает подзапрос «LIMIT & IN / ALL / ANY / SOME» *

Есть идеи?

Ответы [ 3 ]

6 голосов
/ 22 августа 2011

Вам, вероятно, будет проще, если вы просто измените свою схему на что-то вроде:

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

(с другими соответствующими индексами), а затем будете беспокоиться только о каждой стране по очереди.

Другими словами, ваш цикл for имеет уникальный идентификатор / комбинированную страну, поэтому он может просто запросить последнюю дату записи для этого комбинированного списка и добавить новую строку, если она отличается.

Это сделает ваш выборнемного сложнее, но я считаю, что это лучшее решение, особенно , если есть вероятность, что в будущем может быть добавлено больше стран (в этом случае это не нарушит схему).

2 голосов
/ 22 августа 2011

Во-первых, я предлагаю вам хранить данные в форме, которая менее жестко закодирована для каждой страны:

+----------+--------------+------------+-------+
| 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. Если нет, вставьте одну

Была ли проблема с этим подходом?

Надеюсь, это поможет.

1 голос
/ 07 сентября 2011

После экспериментов и с некоторой помощью Условная вставка MySQL и http://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 в объединенной таблице (не удалось присоединиться).

После того, как вы выбрали все строки с последней отметкой времени, возьмите только те строки, где steam_id является steam_idвы ищете, и где цена отличается от новой цены, которую вы вводите.Если в данный момент для этой игры нет строк с другой ценой, то цена не изменилась с момента последнего обновления, поэтому возвращается пустой набор.Когда возвращается пустой набор, инструкция SELECT не выполняется и ничего не вставляется.Если инструкция SELECT выполнена успешно (была найдена другая цена), она возвращает строку 7870, 'us', 999, NOW(), которая вставлена ​​в нашу таблицу.

EDIT - я действительно обнаружил ошибку с вышеуказанным запросом через некоторое время, и яс тех пор пересмотрел это.Приведенный выше запрос вставит новую строку, если цена изменилась с момента последнего обновления, но не вставит строку, если в базе данных в настоящее время нет цен для этого элемента.

Чтобы решить эту проблему, мне пришлосьВоспользуйтесь таблицей DUAL (которая всегда содержит одну строку), затем используйте ИЛИ в предложении where для проверки другой цены ИЛИ пустой набор

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, то она вставляет новую строку.Если цена уже существует, она проверяет цену с последним обновлением и, если отличается, вставляет новую строку.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...