5

I have a hierarchyid with the value '/1/'. When I insert it into the SQL Database it's stored as 0x58.

Here is my table:

CREATE TABLE [dbo].[Category](
    [CategoryId] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](max) NULL,
    [HierarchyDescription] [hierarchyid] NULL,
 CONSTRAINT [PK_Category] PRIMARY KEY CLUSTERED 
(
    [CategoryId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

And here is how I do it:

INSERT INTO [dbo].[Category] ([Name] ,[HierarchyDescription])
VALUES ('CAT1', '/1/');

How is the value 0x58 obtained?

I tried doing this in C#

Encoding.ASCII.GetBytes("/1/");

And I got the value 0x2F312F (47 49 47) in Hex.

Martin Smith
  • 438,706
  • 87
  • 741
  • 845
Decano
  • 93
  • 6
  • Really `select cast(0x58 as [hierarchyid]) bh` is `/1/`. Never saw it documented but [c# example](https://learn.microsoft.com/en-us/sql/t-sql/data-types/read-database-engine?view=sql-server-ver15) – Serg Jan 05 '22 at 15:52
  • 1
    What problem are you trying to solve? That is, once you know what the internal structure of an opaque data type is, what does that allow you do to? – Ben Thul Jan 05 '22 at 18:58
  • Basically I want to create a byte[] in c#, that could be stored in SQL Server as a HierarchyId – Decano Jan 07 '22 at 10:46

2 Answers2

2

HierarchyId is documented as Extremely compact

The average number of bits that are required to represent a node in a tree with n nodes depends on the average fanout (the average number of children of a node). For small fanouts (0-7), the size is about 6*logAn bits, where A is the average fanout. A node in an organizational hierarchy of 100,000 people with an average fanout of 6 levels takes about 38 bits. This is rounded up to 40 bits, or 5 bytes, for storage.

So it is clear that it can manage to fit more than one level in a byte.

The internal format isn't documented but someone has previously investigated this (way back machine link as the original page didn't load when I looked at it).

Disclaimer: The source in itself is non authoritative and just deduced from inspection of values and I may have introduced further errors in the below!

The below gives some fairly simple examples

WITH HierarchyIdStrings(path) As
(
SELECT '/'  UNION ALL
SELECT '/0/'  UNION ALL
SELECT '/1/'  UNION ALL
SELECT '/1/1/'  UNION ALL
SELECT '/1.1/'  UNION ALL
SELECT '/2/'    UNION ALL
SELECT '/98/'  
)
SELECT HierarchyId::Parse(path) AS HierarchyId, path, 
bin,
trimmed_bin = LEFT(bin, 1 + LEN(bin) - CHARINDEX('1', REVERSE(bin)))
FROM HierarchyIdStrings
CROSS APPLY(SELECT CONVERT(VARCHAR(1784), CAST(HierarchyId::Parse(path) AS VARBINARY(892)), 2)) V1(hex)
CROSS APPLY (SELECT 
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
hex,
 '0', '0000'),'1', '0001'),'2', '0010'),'3', '0011'),
 '4', '0100'),'5', '0101'),'6', '0110'),'7', '0111'),
 '8', '1000'),'9', '1001'),'A', '1010'),'B', '1011'),
 'C', '1100'),'D', '1101'),'E', '1110'),'F', '1111') ) V2(bin)
 ORDER BY HierarchyId

Results

+-------------+-------+--------------------------+--------------------+
| HierarchyId | path  |           bin            |    trimmed_bin     |
+-------------+-------+--------------------------+--------------------+
| 0x          | /     |                          |                    |
| 0x48        | /0/   |                 01001000 |              01001 |
| 0x58        | /1/   |                 01011000 |              01011 |
| 0x5AC0      | /1/1/ |         0101101011000000 |         0101101011 |
| 0x62C0      | /1.1/ |         0110001011000000 |         0110001011 |
| 0x68        | /2/   |                 01101000 |              01101 |
| 0xE02540    | /98/  | 111000000010010101000000 | 111000000010010101 |
+-------------+-------+--------------------------+--------------------+

The binary representation of 0x58 is 01011000 but trailing zeroes should be ignored so we only have 5 bits to care about. 01011.

The linked article supposes that the initial prefix 01 is reserved for components /0/ ... /3/ so it knows it only has to read three more bits for this component. The next two bits are for the value itself and the final bit is 1 because this is not a dotted component.

The representation of /1/1/ just concatenates two of these together.

The representation of 1.1 adds 1 to 01011 to get 01100 then concatenates a 01011 onto the end of that.

/98/ is more complicated. This gives a bit string of 111000000010010101.

The linked article indicates that the range /80/ through /1103/ will have the prefix 1110 and be in the form 1110aaa0zzz0y1xxx1

  • aaa = 000 (decimal 0)
  • zzz = 001 (decimal 1)
  • y = 0 (decimal 0)
  • xxx = 010 (decimal 2)

1x + 8y + 16z + 80 = 2 + 0 + 16 + 80 = 98

There is also the possibility that the component may have a negative number. See the linked article if interested in that!

Martin Smith
  • 438,706
  • 87
  • 741
  • 845
  • I like your answer because it explains how it works, what I actually did is using a MemoryStream to get the byte[]. I think you described how the MemoryStream treats a string with the HierarchyId format – Decano Jan 07 '22 at 10:56
  • yeah `Microsoft.SqlServer.Types` assembly knows the binary format - which I assume is what you are using. But that doesn't answer the question you asked as you asked "How is HierachyId stored in SQL Server?" and calling a method on the `SqlHierarchyId` class doesn't answer that. It still leaves the format as a mystery – Martin Smith Jan 07 '22 at 11:01
  • Yes, you are right, I forgot I was calling the Write from HierarchyId, I thouth it was just a generic write of a string to a BinaryWriter. My objective was to reproduce the stored value in C# – Decano Jan 07 '22 at 11:12
0

I solved it by doing the following, there may be a better way

hierarchyValue = "/1/";
MemoryStream memStream = new MemoryStream();
BinaryWriter binWriter = new BinaryWriter(memStream);
hierarchyValue.Write(binWriter);
byte[] propertyValue = memStream.ToArray();

The value stored in the database for the HierarchyId is propertyValue

Shunya
  • 2,344
  • 4
  • 16
  • 28
Decano
  • 93
  • 6