MySQL ON DUPLICATE KEY UPDATE с нулевым столбцом в уникальном ключе

Наша база данных веб-аналитики MySQL содержит сводную таблицу, которая обновляется в течение дня, когда импортируется новое мероприятие. Мы используем ON DUPLICATE KEY UPDATE, чтобы обобщение перезаписывало более ранние вычисления, но испытывает трудности, поскольку один из столбцов в сводной таблице UNIQUE KEY является необязательным FK и содержит значения NULL.

Эти NULL предназначены для обозначения "нет, и все такие случаи эквивалентны". Конечно, MySQL обычно рассматривает NULL как значение "неизвестно, и все такие случаи не эквивалентны".

Базовая структура выглядит следующим образом:

Таблица "Активность", содержащая запись для каждого сеанса, каждая из которых принадлежит кампании, с дополнительными фильтрами и идентификаторами транзакций для некоторых записей.

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`)
);

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

Фактический запрос суммирования выглядит примерно так: подсчет количества сеансов и транзакций, а затем группировка по кампании и (необязательный) фильтр.

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".

Я ищу идеи для обходных решений или отзывов о тех, с которыми мы пришли. Обходные решения, о которых мы думали, до сих пор следуют.

  • Удалите все итоговые записи, содержащие значение ключа NULL, перед запуском сводки. (Это то, что мы делаем сейчас) Это имеет отрицательный побочный эффект при возврате результатов с отсутствующими данными, если запрос выполняется во время процесса суммирования.

  • Измените столбец DEFAULT NULL на DEFAULT 0, который позволяет последовательно использовать UNIQUE KEY. Это имеет отрицательный побочный эффект чрезмерного усложнения разработки запросов к сводной таблице. Это заставляет нас использовать много "CASE filter_id = 0 THEN NULL ELSE filter_id END" и делает неудобное соединение, поскольку все остальные таблицы имеют фактические значения NULL для filter_id.

  • Создайте представление, которое возвращает "CASE filter_id = 0 THEN NULL ELSE filter_id END" и непосредственно использует этот вид вместо таблицы. Сводная таблица содержит несколько сотен тысяч строк, и мне сказали, что производительность представления довольно плохая.

  • Разрешить создание повторяющихся записей и удалять старые записи после завершения сводки. Имеются аналогичные проблемы для их удаления заблаговременно.

  • Добавьте суррогатный столбец, содержащий 0 для NULL, и используйте этот суррогат в UNIQUE KEY (на самом деле мы могли бы использовать PRIMARY KEY, если все столбцы NOT NULL).
    Это решение кажется разумным, за исключением того, что приведенный выше пример является лишь примером; фактическая база данных содержит полдюжины сводных таблиц, одна из которых содержит четыре столбца с нулевым значением в UNIQUE KEY. Некоторые обеспокоены тем, что накладные расходы слишком много.

У вас есть лучшее обходное решение, структура таблицы, процесс обновления или наилучшая практика MySQL, которая может помочь?

EDIT: Чтобы прояснить "значение null"

Данные в сводных строках, содержащих столбцы NULL, считаются принадлежащими друг другу только в том смысле, что они представляют собой единую строку "catch-all" в сводных отчетах, суммируя те элементы, для которых эта точка данных не существует или неизвестна, Таким образом, в контексте самой сводной таблицы значение означает "сумма тех записей, для которых неизвестно значение". С другой стороны, в реляционных таблицах это действительно NULL-результаты.

Единственная причина поместить их в уникальный ключ в сводной таблице - это позволить автоматическое обновление (путем включения DUPLICATE KEY UPDATE) при повторном подсчете итоговых отчетов.

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

Изменение остальных таблиц данных с явным значением "THERE_IS_NO_ZIP_CODE" 0 и значением специальной записи в таблице ZipCodePrefix, представляющей это значение, является неправильным - это отношение действительно равно NULL.

Ответ 1

Я думаю, что что-то в соответствии с (2) действительно лучший выбор - или, по крайней мере, это было бы, если бы вы начали с нуля. В SQL NULL означает неизвестный. Если вам нужен какой-то другой смысл, вы действительно должны использовать для этого особое значение, а 0 - выбор ОК.

Вы должны сделать это по всей базе данных, а не только по одной таблице. Тогда вы не должны заканчиваться необычными частными случаями. Фактически, вы должны быть в состоянии избавиться от многих ваших текущих (пример: в настоящее время, если вы хотите, чтобы сводная строка, где нет фильтра, у вас есть специальный случай "filter is null", а не обычный случай "filter =?".)

Вы также должны продолжить и создать запись "нет" в упомянутой таблице, чтобы сохранить ограничение FK действительным (и избегать особых случаев).

PS: Таблицы без первичного ключа не являются реляционными таблицами, и их действительно следует избегать.

изменить 1

Хм, в таком случае вам действительно нужно дублировать обновление ключа? Если вы делаете INSERT... SELECT, то вы, вероятно, это делаете. Но если ваше приложение предоставляет данные, просто сделайте это вручную - сделайте обновление (сопоставление zip = null - zip is null), проверьте, сколько строк было изменено (MySQL возвращает это), если 0 делает вставку.

Ответ 2

Измените столбец DEFAULT NULL на DEFAULT 0, который позволяет последовательно использовать UNIQUE KEY. Это имеет отрицательный побочный эффект чрезмерного усложнения разработки запросов к сводной таблице. Это заставляет нас использовать много "CASE filter_id = 0 THEN NULL ELSE filter_id END" и делает неудобное соединение, поскольку все остальные таблицы имеют фактические значения NULL для filter_id.

Создайте представление, которое возвращает "CASE filter_id = 0 THEN NULL ELSE filter_id END" и непосредственно использует это представление вместо таблицы. Сводная таблица содержит несколько сотен тысяч строк, и мне сказали, что производительность представления довольно плохая.

Просмотр производительности в MySQL 5.x будет прекрасным, так как представление ничего не делает, кроме нуля с нулевым значением. Если вы не используете агрегаты/сортировки в представлении, большинство запросов к представлению будут переписаны оптимизатором запросов, чтобы просто попасть в базовую таблицу.

И, конечно, поскольку это FK, вам нужно будет создать запись в указанной таблице с нулевым идентификатором.

Ответ 3

С современными версиями MariaDB (ранее MySQL), upserts можно сделать просто с вставкой в ​​дубликаты ключевых операторов обновления, если вы идете с суррогатной колонкой маршрута №5. Добавление MySQL сгенерированных хранимых столбцов или постоянных виртуальных столбцов MariaDB для применения ограничения уникальности по полям с возможностью сбрасывания косвенно держит бессмысленные данные из базы данных в обмен на некоторое раздувание.

например.

CREATE TABLE IF NOT EXISTS bar (
    id INT PRIMARY KEY AUTO_INCREMENT,
    datebin DATE NOT NULL,
    baz1_id INT DEFAULT NULL,
    vbaz1_id INT AS (COALESCE(baz1_id, -1)) STORED,
    baz2_id INT DEFAULT NULL,
    vbaz2_id INT AS (COALESCE(baz2_id, -1)) STORED,
    blam DOUBLE NOT NULL,
    UNIQUE(datebin, vbaz1_id, vbaz2_id)
);

INSERT INTO bar (datebin, baz1_id, baz2_id, blam)
    VALUES ('2016-06-01', null, null, 777)
ON DUPLICATE KEY UPDATE
    blam = VALUES(blam);

Для MariaDB замените STORED на PERSISTENT, индексы требуют сохранения.

Созданные столбцы MySQL Виртуальные столбцы MariaDB