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 ...
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])
);
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 ...
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: