web-dev-qa-db-ja.com

親子関係テーブルの設計-ベストプラクティスとは

「タスク」を保存するための単一のテーブルがあります。タスクは親および/または子にすることができます。同じテーブルのPKを参照するFKとして 'ParentID'を使用します。 NULLABLEなので、[〜#〜] null [〜#〜]の場合、親タスクはありません。

例は下のスクリーンショットです...

enter image description here

私のチームでは、ParentIDを格納する別のテーブルを作成し、テーブルにNULLSが含まれないようにして正規化設計を改善する方が(正規化/ベストプラクティスにとって)はるかに優れていると主張されています。

これはより良いオプションでしょうか?それとも、クエリが難しくなり、パフォーマンスの問題が発生しますか?

後で問題を見つけるのではなく、設計を最初から正しくしたいだけです。

既存のテーブルのSQL-DDLコード:

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

タスク階層を表すために説明している手法は、「隣接リスト」と呼ばれます。人間にとって最も直感的ですが、SQLでの非常に効率的なクエリには適していません。他の手法には、パス列挙(別名マテリアライズドパス)とネストされたセットが含まれます。他のいくつかのテクニックについて学ぶには、 この投稿 を読むか、これらのテクニックに関する多数の記事をウェブで検索してください。

SQL Serverは、パス列挙のために ネイティブ階層表現 を提供します。これはおそらくあなたの最善の策です...

7
SQLRaptor

Null値を禁止したり、隣接リストを別のテーブルに格納する必要がある正規化のルールはありません。どちらのアプローチも一般的であり、選択したパフォーマンスに大きな影響はありません。

どちらのデザインを選択する場合でも、すべての外部キー列はインデックスでサポートされる必要があることに注意してください。したがって、階層を効率的にたどることができるように、ParentIDのインデックスが必要になります。

SQL Server 2017およびAzure SQL DBから、 新しいグラフデータベース機能 と新しいMATCH句を使用して、このタイプの関係をモデル化できます。ヌルを見てはいけません!サンプルスクリプト:

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
    )

私の結果:

My results

2
wBob