Pergunta

Nosso banco de dados de análise da web MySQL contém uma tabela de resumo que é atualizada ao longo do dia à medida que novas atividades são importadas.Usamos ON DUPLICATE KEY UPDATE para que o resumo substitua os cálculos anteriores, mas estamos tendo dificuldades porque uma das colunas na UNIQUE KEY da tabela de resumo é um FK opcional e contém valores NULL.

Esses NULLs pretendem significar "não presente e todos esses casos são equivalentes".Claro, o MySQL geralmente trata NULLs como significando "desconhecido, e todos esses casos não são equivalentes".

A estrutura básica é a seguinte:

Uma tabela "Atividade" contendo uma entrada para cada sessão, cada uma pertencente a uma campanha, com filtro opcional e IDs de transação para algumas entradas.

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

Uma tabela de "Resumo" contendo rollups diários do número total de sessões na tabela de atividades e o número total dessas sessões que contêm um ID de transação.Esses resumos são divididos, um para cada combinação de campanha e filtro (opcional).Esta é uma tabela não transacional usando 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;

A consulta de resumo real é semelhante à seguinte, contando o número de sessões e transações e, em seguida, agrupando por campanha e filtro (opcional).

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

Tudo funciona muito bem, exceto o resumo dos casos em que o filter_id é NULL.Nestes casos, a cláusula ON DUPLICATE KEY UPDATE não corresponde à linha existente e uma nova linha é gravada sempre.Isso se deve ao fato de que "NULL! = NULL".O que precisamos, entretanto, é "NULL = NULL" ao comparar as chaves exclusivas.

Estou procurando ideias para soluções alternativas ou feedback sobre aquelas que criamos até agora.Seguem soluções alternativas que pensamos até agora.

  1. Exclua todas as entradas de resumo que contenham um valor de chave NULL antes de executar o resumo.(É isso que estamos fazendo agora) Isso tem o efeito colateral negativo de retornar os resultados com dados ausentes se uma consulta for executada durante o processo de resumo.

  2. Altere a coluna DEFAULT NULL para DEFAULT 0, o que permite que a UNIQUE KEY seja correspondida de forma consistente.Isso tem o efeito colateral negativo de complicar excessivamente o desenvolvimento de consultas na tabela de resumo.Isso nos força a usar muito "CASE filter_id = 0 THEN NULL ELSE filter_id END" e torna a junção estranha, já que todas as outras tabelas têm NULLs reais para o filter_id.

  3. Crie uma visualização que retorne "CASE filter_id = 0 THEN NULL ELSE filter_id END" e use esta visualização em vez da tabela diretamente.A tabela de resumo contém algumas centenas de milhares de linhas e me disseram que o desempenho da visualização é bastante ruim.

  4. Permita que as entradas duplicadas sejam criadas e exclua as entradas antigas após a conclusão do resumo.Tem problemas semelhantes aos de excluí-los antecipadamente.

  5. Adicione uma coluna substituta que contenha 0 para NULL e use esse substituto na UNIQUE KEY (na verdade, poderíamos usar PRIMARY KEY se todas as colunas NÃO fossem NULL).
    Esta solução parece razoável, exceto que o exemplo acima é apenas um exemplo;o banco de dados real contém meia dúzia de tabelas de resumo, uma das quais contém quatro colunas anuláveis ​​na UNIQUE KEY.Há preocupação por parte de alguns de que a sobrecarga seja excessiva.

Você tem uma solução alternativa melhor, estrutura de tabela, processo de atualização ou práticas recomendadas de MySQL que podem ajudar?

EDITAR:Para esclarecer o "significado de nulo"

Os dados nas linhas de resumo contendo colunas NULL são considerados pertencentes um ao outro apenas no sentido de serem uma única linha "pega-tudo" em relatórios de resumo, resumindo os itens para os quais esse ponto de dados não existe ou é desconhecido.Portanto, no contexto da própria tabela de resumo, o significado é "a soma das entradas para as quais nenhum valor é conhecido".Nas tabelas relacionais, por outro lado, esses resultados são realmente NULOS.

A única razão para colocá-los em uma chave exclusiva na tabela de resumo é permitir a atualização automática (por ON DUPLICATE KEY UPDATE) ao recalcular os relatórios de resumo.

Talvez a melhor maneira de descrevê-lo seja pelo exemplo específico de que uma das tabelas de resumo agrupa os resultados geograficamente pelo prefixo do CEP do endereço comercial fornecido pelo respondente.Nem todos os entrevistados fornecem um endereço comercial, portanto, o relacionamento entre a tabela de transações e endereços é corretamente NULO.Na tabela de resumo desses dados, é gerada uma linha para cada prefixo de CEP, contendo o resumo dos dados dessa área.Uma linha adicional é gerada para mostrar o resumo dos dados para os quais nenhum prefixo de CEP é conhecido.

Alterar o restante das tabelas de dados para ter um valor 0 "THERE_IS_NO_ZIP_CODE" explícito e colocar um registro especial na tabela ZipCodePrefix representando esse valor é impróprio - esse relacionamento é realmente NULL.

Foi útil?

Solução

Acho que algo como (2) é realmente a melhor aposta - ou, pelo menos, seria se você estivesse começando do zero.Em SQL, NULL significa desconhecido.Se você quiser algum outro significado, você realmente deveria usar um valor especial para isso, e 0 é certamente uma boa escolha.

Você deve fazer isso em todo o inteiro banco de dados, não apenas esta tabela.Então você não deve acabar com casos especiais estranhos.Na verdade, você deve conseguir se livrar de muitos dos atuais (exemplo:atualmente, se você deseja a linha de resumo onde não há filtro, você tem o caso especial "filtro é nulo" em oposição ao caso normal "filtro =?".)

Você também deve prosseguir e criar uma entrada "não presente" na tabela referida, para manter a restrição FK válida (e evitar casos especiais).

PS:Tabelas sem chave primária não são tabelas relacionais e devem realmente ser evitadas.

editar 1

Hmmm, nesse caso, você realmente precisa da atualização da chave duplicada?Se você estiver fazendo um INSERT ...SELECT, então provavelmente você o faz.Mas se o seu aplicativo estiver fornecendo os dados, faça isso manualmente - faça a atualização (mapeamento zip = null para zip is null), verifique quantas linhas foram alteradas (o MySQL retorna isso), se 0 faça uma inserção.

Outras dicas

Altere a coluna DEFAULT NULL para DEFAULT 0, o que permite que a UNIQUE KEY seja correspondida de forma consistente.Isso tem o efeito colateral negativo de complicar excessivamente o desenvolvimento de consultas na tabela de resumo.Isso nos força a usar muito "CASE filter_id = 0 THEN NULL ELSE filter_id END" e torna a junção estranha, já que todas as outras tabelas têm NULLs reais para o filter_id.

Crie uma visualização que retorne "CASE filter_id = 0 THEN NULL ELSE filter_id END" e use esta visualização em vez da tabela diretamente.A tabela de resumo contém algumas centenas de milhares de linhas e me disseram que o desempenho da visualização é bastante ruim.

O desempenho da visualização no MySQL 5.x será bom, pois a visualização não faz nada além de substituir zero por nulo.A menos que você use agregações/classificações em uma visualização, quase todas as consultas na visualização serão reescritas pelo otimizador de consulta para atingir apenas a tabela subjacente.

E claro, por se tratar de um FK, você terá que criar uma entrada na referida tabela com id zero.

Com versões modernas do MariaDB (anteriormente MySQL), upserts podem ser feitos simplesmente inserindo instruções de atualização de chave duplicada se você usar a rota de coluna substituta nº 5.Adicionar colunas armazenadas geradas pelo MySQL ou colunas virtuais persistentes do MariaDB para aplicar a restrição de exclusividade nos campos anuláveis ​​mantém indiretamente dados sem sentido fora do banco de dados em troca de algum inchaço.

por exemplo.

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

Para MariaDB substitua STORED por PERSISTENT, os índices requerem persistência.

Colunas geradas pelo MySQL Colunas Virtuais MariaDB

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top