MySQL на дублирующем ключевом обновлении с nullable столбцом в уникальном ключе

наша база данных MySQL web analytics содержит сводную таблицу, которая обновляется в течение дня по мере импорта новой активности. Мы используем при обновлении дубликатов ключей, чтобы суммирование перезаписывало более ранние вычисления, но испытывает трудности, потому что один из столбцов уникального ключа сводной таблицы является необязательным FK и содержит нулевые значения.

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

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

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

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

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

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

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

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

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

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

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

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

EDIT: чтобы уточнить "значение null"

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

единственная причина, по которой они помещаются в уникальный ключ в сводной таблице,-это автоматическое обновление (по дубликату ключа) при повторном расчете сводных отчетов.

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

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

3 ответов


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

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

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

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

изменить 1

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


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

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

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

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


С современными версиями MariaDB (ранее MySQL), upserts можно сделать просто с insert on duplicate key update statements, если вы идете с суррогатным столбцом route #5. Добавление сгенерированных хранимых столбцов MySQL или постоянных виртуальных столбцов MariaDB для применения ограничения уникальности для полей nullable косвенно сохраняет бессмысленные данные из базы данных в обмен на некоторое раздувание.

например

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 замените сохраненные на постоянные, индексы требуется настойчивость.

MySQL Генерируемые Столбцы Виртуальные Столбцы MariaDB