Design der Eltern-Kind-Beziehungstabelle - Was ist die beste Vorgehensweise?

7

Ich habe eine einzige Tabelle zum Speichern von 'Aufgaben'. Eine Aufgabe kann ein Elternteil und / oder ein Kind sein. Ich verwende die ' ParentID ' als FK, die auf die PK in derselben Tabelle verweist. Es ist NULLABLE. Wenn es also NULL ist, hat es keine übergeordnete Aufgabe.

Beispiel ist der Screenshot unten ...

Geben Sie hier die Bildbeschreibung ein

In meinem Team wurde argumentiert, dass es (für Normalisierung / Best Practices) viel besser wäre, eine separate Tabelle zum Speichern der ParentIDs zu erstellen, um NULL-Werte in der Tabelle zu vermeiden und zu einem besseren Normalisierungsdesign zu führen.

Wäre dies eine bessere Option? Oder wird es schwieriger, Abfragen durchzuführen und Leistungsprobleme zu verursachen?

Wir wollen nur das Design von Anfang an richtig machen, anstatt später Probleme zu finden.

SQL-DDL-Code für die vorhandene Tabelle:

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])
);
Kevin
quelle
2
Bin ich der einzige, der erwartet hat, dass diese Frage bei Parenting SE gestellt wird ?
Martin Hennings
ist verschachtelte Eltern / Kind-Beziehung? Können 3 in Ihrem Beispiel Eltern von 4 sein und so weiter? Wenn es verschachtelt werden kann, was bringt es dann, eine untergeordnete Tabelle zu erstellen? Auch wenn es verschachtelt ist, können Sie eine Spalte mit dem Namen "Level" behalten, die einen vorberechneten Wert enthält und hilft. Wenn es nicht verschachtelt ist, werden Sie keine sehen Abfrage-Herausforderung. Finden Sie heraus, ob nullable parented als vertrauenswürdig eingestuft wird oder nicht. Eine Logik für eine separate Tabelle lautet: Wenn die Tabelle Millionen von Milliarden Datensätzen enthält, ist es besser, sie getrennt zu halten.
Kumar Harsh

Antworten:

7

Die Technik, die Sie zur Darstellung der Aufgabenhierarchie beschreiben, wird als "Adjazenzliste" bezeichnet. Obwohl es für Menschen am intuitivsten ist, eignet es sich nicht für sehr effiziente Abfragen in SQL. Andere Techniken umfassen Pfadaufzählung (auch als materialisierte Pfade bezeichnet) und verschachtelte Mengen. Um mehr über andere Techniken zu erfahren, lesen Sie diesen Beitrag oder durchsuchen Sie das Internet nach zahlreichen Artikeln zu diesen Techniken.

SQL Server bietet eine native Hierarchiedarstellung für die Pfadaufzählung. Dies ist höchstwahrscheinlich Ihre beste Wahl ...

SQLRaptor
quelle
3

Es gibt keine Normalisierungsregel, die Nullwerte verbietet oder das Speichern der Adjazenzliste in einer separaten Tabelle erfordern würde. Beide Ansätze sind üblich, und Ihre Wahl hat keine wesentlichen Auswirkungen auf die Leistung.

Unabhängig vom gewählten Design müssen alle Fremdschlüsselspalten von einem Index unterstützt werden. Sie benötigen also einen Index für ParentID, um ein effizientes Durchlaufen der Hierarchie zu unterstützen.

David Browne - Microsoft
quelle
2

Ab SQL Server 2017 und Azure SQL DB können Sie die neuen Diagrammdatenbankfunktionen und die neue MATCH-Klausel verwenden, um diese Art von Beziehung zu modellieren. Schau keine Nullen! Ein Beispielskript:

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
    )

Meine Ergebnisse:

Meine Ergebnisse

wBob
quelle