web-dev-qa-db-fra.com

Conception de la table des relations parents / enfants - Quelle est la meilleure pratique?

J'ai une seule table pour stocker les "tâches". Une tâche peut être un parent et/ou un enfant. J'utilise le ' ParentID' comme FK référençant le PK sur la même table. Il est NULLABLE, donc s'il est [~ # ~] null [~ # ~] il n'a pas de tâche parent.

L'exemple est la capture d'écran ci-dessous ...

enter image description here

Il a été soutenu dans mon équipe, qu'il serait beaucoup mieux (pour la normalisation/les meilleures pratiques) de créer une table distincte pour stocker les ParentID et ainsi éviter d'avoir NULLS dans la table et conduire à une meilleure conception de normalisation.

Serait-ce une meilleure option? Ou sera-t-il plus difficile d'interroger et de provoquer des problèmes de performances?

Nous voulons juste obtenir la conception dès le début plutôt que de trouver des problèmes plus tard.

Code SQL-DDL pour la table existante:

CREATE TABLE [Tasks].[TaskDetail]
(
    [TaskDetailID] [int] IDENTITY(1,1) NOT NULL,
    [TaskName] [varchar](50) NOT NULL,
    [TaskDescription] [varchar](250) NULL,
    [IsActive] [bit] NOT NULL CONSTRAINT [DF_TaskDetail_IsActive] DEFAULT ((1)),
    [ParentID] [int] NULL,
    CONSTRAINT [PK_TaskDetail_TaskDetailID] PRIMARY KEY CLUSTERED ([TaskDetailID] ASC),
    CONSTRAINT [FK_TaskDetail_ParentID] FOREIGN KEY([ParentID]) REFERENCES [Tasks].[TaskDetail]([TaskDetailID])
);
7
Kevin

La technique que vous décrivez pour représenter la hiérarchie des tâches est appelée "liste d'adjacence". Bien qu'il soit le plus intuitif pour l'homme, il ne se prête pas à des requêtes très efficaces en SQL. D'autres techniques incluent l'énumération des chemins (également appelés chemins matérialisés) et les ensembles imbriqués. Pour en savoir plus sur certaines autres techniques, lisez cet article ou recherchez sur le Web de nombreux articles sur ces techniques.

SQL Server propose une représentation de la hiérarchie native pour l'énumération des chemins. C'est probablement votre meilleur pari ...

7
SQLRaptor

Aucune règle de normalisation n'interdit les valeurs nulles ou nécessiterait de stocker la liste d'adjacence dans une table distincte. Les deux approches sont courantes et votre choix n'a aucune incidence significative sur les performances.

Quelle que soit la conception que vous choisissez, n'oubliez pas que toutes les colonnes de clés étrangères doivent être prises en charge par un index. Vous aurez donc besoin d'un index sur ParentID pour prendre en charge une traversée efficace dans la hiérarchie.

À partir de SQL Server 2017 et Azure SQL DB, vous pouvez utiliser nouvelles fonctionnalités de base de données de graphes et la nouvelle clause MATCH pour modéliser ce type de relation. Ne cherchez pas de null! Un exemple de script:

USE tempdb
GO

IF NOT EXISTS ( SELECT * FROM sys.schemas WHERE name = 'Tasks' )
EXEC('CREATE SCHEMA Tasks')
GO

IF NOT EXISTS ( SELECT * FROM sys.schemas WHERE name = 'graph' )
EXEC('CREATE SCHEMA graph')
GO

DROP TABLE IF EXISTS [Tasks].[TaskDetail]
DROP TABLE IF EXISTS graph.taskDetail
DROP TABLE IF EXISTS graph.isParentOf 
DROP TABLE IF EXISTS graph.isChildOf 
GO

CREATE TABLE [Tasks].[TaskDetail]
(
    [TaskDetailID] [int] IDENTITY(1,1) NOT NULL,
    [TaskName] [varchar](50) NOT NULL,
    [TaskDescription] [varchar](250) NULL,
    [IsActive] [bit] NOT NULL CONSTRAINT [DF_TaskDetail_IsActive] DEFAULT ((1)),
    [ParentID] [int] NULL,
    CONSTRAINT [PK_TaskDetail_TaskDetailID] PRIMARY KEY CLUSTERED ([TaskDetailID] ASC),
    CONSTRAINT [FK_TaskDetail_ParentID] FOREIGN KEY([ParentID]) REFERENCES [Tasks].[TaskDetail]([TaskDetailID])
);
GO

SET IDENTITY_INSERT [Tasks].[TaskDetail] ON 
GO

INSERT INTO [Tasks].[TaskDetail] ( TaskDetailID, TaskName, TaskDescription, IsActive, ParentID )
VALUES 
    ( 2, 'Cash Receipt 1', 'Fund Account', 1, NULL ),
    ( 3, 'Cash Receipt 2', 'Check the ...', 1, 2 ),
    ( 4, 'Non Trade', 'Income & Expense', 1, NULL ),
    ( 5, 'Income Verified', 'Income Verified', 1, 4 ),
    ( 6, 'Expense Verified', 'Expense Verified', 1, 4 ),
    ( 7, 'Pricing', 'Pricing Verified', 1, NULL ),
    ( 8, 'Manual Pricing', 'Manual Pricing', 1, 7 ),
    ( 9, 'Missing Pricing', 'Missing Pricing', 1, 7 )
GO

SET IDENTITY_INSERT [Tasks].[TaskDetail] OFF
GO


-- Create graph tables
CREATE TABLE graph.taskDetail (
    taskDetailId    INT PRIMARY KEY,
    taskName        VARCHAR(50) NOT NULL,
    taskDescription VARCHAR(250) NULL,
    isActive        BIT NOT NULL 
    ) AS NODE;

CREATE TABLE graph.isParentOf AS Edge;
CREATE TABLE graph.isChildOf AS Edge;
GO

--  !!TODO add indexes

-- Add the node data
INSERT INTO graph.taskDetail ( taskDetailId, taskName, taskDescription, isActive )
SELECT taskDetailId, taskName, taskDescription, isActive
FROM Tasks.TaskDetail

-- Add the Edge data
INSERT INTO graph.isParentOf ( $from_id, $to_id )
SELECT p.$node_id, c.$node_id
FROM Tasks.TaskDetail td
    INNER JOIN graph.taskDetail c ON td.TaskDetailId = c.taskDetailId
    INNER JOIN graph.taskDetail p ON td.ParentID = p.taskDetailId


-- Add inverse relationship
INSERT INTO graph.isChildOf ( $from_id, $to_id )
SELECT $to_id, $from_id 
FROM graph.isParentOf 
GO


-- Now run the graph queries
SELECT
    FORMATMESSAGE( 'Task [%s](%i) is the parent of [%s](%i)', p.taskName, p.taskDetailId, c.taskName, c.taskDetailId )
FROM graph.taskDetail p, graph.isParentOf isParentOf, graph.taskDetail c
WHERE MATCH ( p-(isParentOf)->c )
ORDER BY 1;


-- Tasks with same parent
-- Tasks 5 and 6 have the same parent 4
-- Tasks 8 and 9 have the same parent 7
SELECT
    FORMATMESSAGE( 'Tasks %i and %i have the same parent %i', t1.taskDetailId, t3.taskDetailId, t2.taskDetailId )
FROM graph.taskDetail t1, graph.isChildOf c1, graph.taskDetail t2, graph.isChildOf c2, graph.taskDetail t3
WHERE MATCH ( t1-(c1)->t2<-(c2)-t3 )
  AND t1.$node_id < t3.$node_id
ORDER BY 1;


-- Find tasks with no parents?
SELECT
    FORMATMESSAGE( 'Task [%s](%i) has no parents.', p.taskName, p.taskDetailId )
FROM graph.taskDetail p
WHERE NOT EXISTS
    (
    SELECT *
    FROM graph.isParentOf isParentOf
    WHERE p.$node_id = isParentOf.$from_id
    )

Mes résultats:

My results

2
wBob