AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / dba / Perguntas / 14388
Accepted
Jeroen
Jeroen
Asked: 2012-03-06 07:06:20 +0800 CST2012-03-06 07:06:20 +0800 CST 2012-03-06 07:06:20 +0800 CST

Tabelas com hierarquia: crie uma restrição para evitar a circularidade por meio de chaves estrangeiras

  • 772

Suponha que temos uma tabela que possui uma restrição de chave estrangeira para si mesma, como:

CREATE TABLE Foo 
    (FooId BIGINT PRIMARY KEY,
     ParentFooId BIGINT,
     FOREIGN KEY([ParentFooId]) REFERENCES Foo ([FooId]) )

INSERT INTO Foo (FooId, ParentFooId) 
VALUES (1, NULL), (2, 1), (3, 2)

UPDATE Foo SET ParentFooId = 3 WHERE FooId = 1

Esta tabela terá os seguintes registros:

FooId  ParentFooId
-----  -----------
1      3
2      1
3      2

Há casos em que esse tipo de design pode fazer sentido (por exemplo, o típico relacionamento "empregado-e-chefe-empregado") e, em qualquer caso: Estou em uma situação em que tenho isso em meu esquema.

Infelizmente, esse tipo de design permite a circularidade nos registros de dados, conforme mostrado no exemplo acima.

Minha pergunta então é:

  1. É possível escrever uma restrição que verifique isso? e
  2. É viável escrever uma restrição que verifique isso? (se necessário apenas até uma certa profundidade)

Para a parte (2) desta questão, pode ser relevante mencionar que espero apenas centenas ou talvez, em alguns casos, milhares de registros em minha tabela, normalmente não aninhados em níveis mais profundos do que 5 a 10.

sql-server-2008 constraint
  • 6 6 respostas
  • 5383 Views

6 respostas

  • Voted
  1. John Gietzen
    2012-03-13T18:37:56+08:002012-03-13T18:37:56+08:00

    Eu vi 2 maneiras principais de impor isso:

    1, o jeito ANTIGO:

    CREATE TABLE Foo 
        (FooId BIGINT PRIMARY KEY,
         ParentFooId BIGINT,
         FooHierarchy VARCHAR(256),
         FOREIGN KEY([ParentFooId]) REFERENCES Foo ([FooId]) )
    

    A coluna FooHierarchy conteria um valor como este:

    "|1|27|425"
    

    Onde os números são mapeados para a coluna FooId. Você então imporia que a coluna Hierarchy terminasse com "|id" e o restante da string correspondesse a FooHieratchy do PARENT.

    2, a NOVA maneira:

    O SQL Server 2008 tem um novo tipo de dados chamado HierarchyID , que faz tudo isso para você.

    Ele opera da mesma maneira que o modo ANTIGO, mas é tratado com eficiência pelo SQL Server e é adequado para uso como SUBSTITUIÇÃO para sua coluna "ParentID".

    CREATE TABLE Foo 
        (FooId BIGINT PRIMARY KEY,
         FooHierarchy HIERARCHYID )
    
    • 7
  2. A-K
    2012-03-06T09:10:58+08:002012-03-06T09:10:58+08:00

    É meio que possível: você pode invocar uma UDF escalar de sua restrição CHECK e pode detectar ciclos de qualquer tamanho. Infelizmente, essa abordagem é extremamente lenta e pouco confiável: você pode ter falsos positivos e falsos negativos.

    Em vez disso, eu usaria o caminho materializado.

    Outra forma de evitar ciclos é ter um CHECK(ID > ParentID), o que provavelmente também não é muito viável.

    Outra maneira de evitar ciclos é adicionar mais duas colunas, LevelInHierarchy e ParentLevelInHierarchy, ter (ParentID, ParentLevelInHierarchy) referir-se a (ID, LevelInHierarchy) e ter um CHECK(LevelInHierarchy > ParentLevelInHierarchy).

    • 6
  3. Best Answer
    ypercubeᵀᴹ
    2012-03-06T10:02:14+08:002012-03-06T10:02:14+08:00

    Você está usando o modelo de Lista de Adjacência , onde é difícil aplicar tal restrição.

    Você pode examinar o modelo Nested Set , onde apenas hierarquias verdadeiras podem ser representadas (sem caminhos circulares). Isso tem outras desvantagens, como inserções/atualizações lentas.

    • 6
  4. a1ex07
    2012-03-06T09:12:16+08:002012-03-06T09:12:16+08:00

    Acredito que seja possível:

    create function test_foo (@id bigint) returns bit
    as
    begin
    declare @retval bit;
    
    with t1 as (select @id as FooId, 0 as lvl  
    union all 
     select f.FooId , t1.lvl+1 from t1 
     inner join Foo f ON (f.ParentFooId = t1.FooId)
     where lvl<11) -- you said that max nested level 10, so if there is any circular   
    -- dependency, we don't need to go deeper than 11 levels to detect it
    
     select @retval =
     CASE(COUNT(*)) 
     WHEN 0 THEN 0 -- for records that don't have children
     WHEN 1 THEN 0 -- if a record has children
      ELSE 1 -- recursion detected
     END
     from t1
     where t1.FooId = @id ;
    
    return @retval; 
    end;
    GO
    alter table Foo add constraint CHK_REC1 CHECK (dbo.test_foo(ParentFooId) = 0)
    

    Posso ter perdido alguma coisa (desculpe, não posso testar completamente), mas parece funcionar.

    • 4
  5. ErikE
    2012-03-16T15:45:06+08:002012-03-16T15:45:06+08:00

    Aqui está outra opção: um gatilho que permite atualizações de várias linhas e não impõe ciclos. Ele funciona percorrendo a cadeia ancestral até encontrar um elemento raiz (com pai NULL), provando assim que não há ciclo. É limitado a 10 gerações, pois é claro que um ciclo é infinito.

    Funciona apenas com o conjunto atual de linhas modificadas, portanto, desde que as atualizações não toquem em um grande número de itens muito profundos na tabela, o desempenho não deve ser tão ruim. Ele tem que percorrer todo o caminho da cadeia para cada elemento, portanto, terá algum impacto no desempenho.

    Um gatilho verdadeiramente "inteligente" procuraria ciclos diretamente, verificando se um item alcançou a si mesmo e, em seguida, saltou. No entanto, isso requer a verificação do estado de todos os nós encontrados anteriormente durante cada loop e, portanto, requer um loop WHILE e mais codificação do que eu gostaria de fazer agora. Isso não deve ser realmente mais caro porque a operação normal seria não ter ciclos e, neste caso, será mais rápido trabalhar apenas com a geração anterior em vez de todos os nós anteriores durante cada loop.

    Eu adoraria receber informações de @AlexKuznetsov ou de qualquer outra pessoa sobre como isso funcionaria no isolamento de instantâneo. Eu suspeito que não ficaria muito bem, mas gostaria de entender melhor.

    CREATE TRIGGER TR_Foo_PreventCycles_IU ON Foo FOR INSERT, UPDATE
    AS
    SET NOCOUNT ON;
    SET XACT_ABORT ON;
    
    IF EXISTS (
       SELECT *
       FROM sys.dm_exec_session
       WHERE session_id = @@SPID
       AND transaction_isolation_level = 5
    )
    BEGIN;
      SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    END;
    DECLARE
       @CycledFooId bigint,
       @Message varchar(8000);
    
    WITH Cycles AS (
       SELECT
          FooId SourceFooId,
          ParentFooId AncestorFooId,
          1 Generation
       FROM Inserted
       UNION ALL
       SELECT
          C.SourceFooId,
          F.ParentFooId,
          C.Generation + 1
       FROM
          Cycles C
          INNER JOIN dbo.Foo F
             ON C.AncestorFooId = F.FooId
       WHERE
          C.Generation <= 10
    )
    SELECT TOP 1 @CycledFooId = SourceFooId
    FROM Cycles C
    GROUP BY SourceFooId
    HAVING Count(*) = Count(AncestorFooId); -- Doesn't have a NULL AncestorFooId in any row
    
    IF @@RowCount > 0 BEGIN
       SET @Message = CASE WHEN EXISTS (SELECT * FROM Deleted) THEN 'UPDATE' ELSE 'INSERT' END + ' statement violated TRIGGER ''TR_Foo_PreventCycles_IU'' on table "dbo.Foo". A Foo cannot be its own ancestor. Example value is FooId ' + QuoteName(@CycledFooId, '"') + ' with ParentFooId ' + Quotename((SELECT ParentFooId FROM Inserted WHERE FooID = @CycledFooId), '"');
       RAISERROR(@Message, 16, 1);
       ROLLBACK TRAN;   
    END;
    

    Atualizar

    Eu descobri como evitar uma junção extra de volta à tabela Inserted. Se alguém vir uma maneira melhor de fazer o GROUP BY para detectar aqueles que não contêm um NULL, por favor me avise.

    Também adicionei uma opção para READ COMMITTED se a sessão atual estiver no nível SNAPSHOT ISOLATION. Isso evitará inconsistências, embora, infelizmente, cause maior bloqueio. Isso é inevitável para a tarefa em questão.

    • 2
  6. whiterainbow
    2012-03-06T08:41:53+08:002012-03-06T08:41:53+08:00

    Se seus registros estiverem aninhados em mais de 1 nível, uma restrição não funcionará (presumo que você queira dizer, por exemplo, que o registro 1 é o pai do registro 2 e o registro 3 é o pai do registro 1). A única maneira de fazer isso seria no código pai ou com um gatilho, mas se você estiver olhando para uma tabela grande e vários níveis, isso seria bastante intensivo.

    • 1

relate perguntas

  • Melhores práticas para conectar bancos de dados que estão em diferentes regiões geográficas

  • Quais são as principais causas de deadlocks e podem ser evitadas?

  • Quanto "Padding" coloco em meus índices?

  • Existe um processo do tipo "práticas recomendadas" para os desenvolvedores seguirem para alterações no banco de dados?

  • Downgrade do SQL Server 2008 para 2005

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Como ver a lista de bancos de dados no Oracle?

    • 8 respostas
  • Marko Smith

    Quão grande deve ser o mysql innodb_buffer_pool_size?

    • 4 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    restaurar a tabela do arquivo .frm e .ibd?

    • 10 respostas
  • Marko Smith

    Como usar o sqlplus para se conectar a um banco de dados Oracle localizado em outro host sem modificar meu próprio tnsnames.ora

    • 4 respostas
  • Marko Smith

    Como você mysqldump tabela (s) específica (s)?

    • 4 respostas
  • Marko Smith

    Como selecionar a primeira linha de cada grupo?

    • 6 respostas
  • Marko Smith

    Listar os privilégios do banco de dados usando o psql

    • 10 respostas
  • Marko Smith

    Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Como faço para listar todos os bancos de dados e tabelas usando o psql?

    • 7 respostas
  • Martin Hope
    Mike Walsh Por que o log de transações continua crescendo ou fica sem espaço? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland Listar todas as colunas de uma tabela especificada 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney O MySQL pode realizar consultas razoavelmente em bilhões de linhas? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx Como posso monitorar o andamento de uma importação de um arquivo .sql grande? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison Como você mysqldump tabela (s) específica (s)? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    pedrosanta Listar os privilégios do banco de dados usando o psql 2011-08-04 11:01:21 +0800 CST
  • Martin Hope
    Jonas Como posso cronometrar consultas SQL usando psql? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas Como faço para listar todos os bancos de dados e tabelas usando o psql? 2011-02-18 00:45:49 +0800 CST
  • Martin Hope
    bernd_k Quando devo usar uma restrição exclusiva em vez de um índice exclusivo? 2011-01-05 02:32:27 +0800 CST

Hot tag

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve