SQL Server has a hierarchyid type defined to deal with this kind of system.
Consider this example:
IF OBJECT_ID(N'dbo.Categories', N'U') IS NOT NULL
DROP TABLE dbo.Categories;
GO
CREATE TABLE dbo.Categories
(
CatID int NOT NULL
CONSTRAINT PK_Categories
PRIMARY KEY CLUSTERED
IDENTITY(1,1)
, hid hierarchyid NOT NULL
, CategoryName varchar(30) NOT NULL
);
INSERT INTO dbo.Categories (hid, CategoryName)
VALUES ('/', 'Root')
, ('/1/', 'A')
, ('/1/2/', 'B')
, ('/1/2/3/', 'C')
, ('/1/2/4/', 'D')
, ('/1/2/5/', 'E');
A sample query against the table:
SELECT c.CatID
, Hierarchy = c.hid.ToString()
, Parent = c.hid.GetAncestor(1).ToString()
, c.CategoryName
, ParentCategories = STUFF((
SELECT ', ' + c1.CategoryName
FROM dbo.Categories c1
WHERE c.hid.IsDescendantOf(c1.hid) = CONVERT(bit, 1)
AND c1.hid <> c.hid
AND c1.CategoryName <> 'Root'
FOR XML PATH ('')), 1, 2, '')
FROM dbo.Categories c;
The results:
╔═══════╦═══════════╦════════╦══════════════╦══════════════════╗
║ CatID ║ Hierarchy ║ Parent ║ CategoryName ║ ParentCategories ║
╠═══════╬═══════════╬════════╬══════════════╬══════════════════╣
║ 1 ║ / ║ NULL ║ Root ║ NULL ║
║ 2 ║ /1/ ║ / ║ A ║ NULL ║
║ 3 ║ /1/2/ ║ /1/ ║ B ║ A ║
║ 4 ║ /1/2/3/ ║ /1/2/ ║ C ║ A, B ║
║ 5 ║ /1/2/4/ ║ /1/2/ ║ D ║ A, B ║
║ 6 ║ /1/2/5/ ║ /1/2/ ║ E ║ A, B ║
╚═══════╩═══════════╩════════╩══════════════╩══════════════════╝
To implement this into your system, you'd reference the above table in your table of searches. Something like:
CREATE TABLE dbo.Searches (
id int NOT NULL
CONSTRAINT PK_Searches
PRIMARY KEY
IDENTITY(1,1)
, searchString nvarchar(100)
, timestamp datetimeoffset
CONSTRAINT DF_Sarches_timestamp
DEFAULT (SYSDATETIMEOFFSET())
, CatID int NOT NULL
CONSTRAINT FK_Searches_Category
FOREIGN KEY
REFERENCES dbo.Categories (CatID)
);
To insert a search in Category C:
INSERT INTO dbo.Searches (searchString, CatID)
VALUES ('This is a search', 4);
Query:
SELECT s.searchString
, s.timestamp
, Category = c.CategoryName
, CategoryParents = (
SELECT '/' + c1.CategoryName
FROM dbo.Categories c1
WHERE c.hid.IsDescendantOf(c1.hid) = CONVERT(bit, 1)
AND c1.CategoryName <> 'Root'
AND c1.hid <> c.hid
FOR XML PATH (''))
FROM dbo.Searches s
INNER JOIN dbo.Categories c ON s.CatID = c.CatID;
Results:
╔══════════════════╦══════════╦═════════════════╦════════════════════════════════════╗
║ searchString ║ Category ║ CategoryParents ║ timestamp ║
╠══════════════════╬══════════╬═════════════════╬════════════════════════════════════╣
║ This is a search ║ C ║ /A/B ║ 2018-03-08 15:29:25.2267243 -06:00 ║
╚══════════════════╩══════════╩═════════════════╩════════════════════════════════════╝
If you subsequently want to reparent C, and retain the old version of C, you do this:
INSERT INTO dbo.Categories (hid, CategoryName)
VALUES ('/1/2/', 'C');
Then, when you insert into the newly re-parented category, like this:
INSERT INTO dbo.Searches (searchString, CatID)
VALUES ('This is a search, but with a different parent', 7);
You get:
SELECT s.searchString
, s.timestamp
, Category = c.CategoryName
, CategoryParents = (
SELECT '/' + c1.CategoryName
FROM dbo.Categories c1
WHERE c.hid.IsDescendantOf(c1.hid) = CONVERT(bit, 1)
AND c1.CategoryName <> 'Root'
AND c1.hid <> c.hid
FOR XML PATH (''))
FROM dbo.Searches s
INNER JOIN dbo.Categories c ON s.CatID = c.CatID;
Results:
╔═══════════════════════════════════════════════╦══════════╦═════════════════╦════════════════════════════════════╗
║ searchString ║ Category ║ CategoryParents ║ timestamp ║
╠═══════════════════════════════════════════════╬══════════╬═════════════════╬════════════════════════════════════╣
║ This is a search ║ C ║ /A/B/C ║ 2018-03-08 15:29:25.2267243 -06:00 ║
║ This is a search, but with a different parent ║ C ║ /A ║ 2018-03-08 15:29:25.2546739 -06:00 ║
╚═══════════════════════════════════════════════╩══════════╩═════════════════╩════════════════════════════════════╝