Why is XML taking up more storage than VARCHAR(MAX)? We have large tables storing XML data as varchar(MAX). The data is for reference/historical purposes, it's not queried. Based on what I've read, storing as XML datatype instead of VARCHAR(MAX) should result in space savings, but my tests show otherwise.  See below, where the size of t1_XML is smaller than t1_NVARCHARMAX, but larger than t1_VARCHARMAX.

set nocount on;

drop table t1_XML;
drop table t1_VARCHARMAX;
drop table t1_NVARCHARMAX;

create table t1_XML(col1 int identity primary key, col2 XML);
create table t1_VARCHARMAX(col1 int identity primary key, col2 varchar(max));
create table t1_NVARCHARMAX(col1 int identity primary key, col2 nvarchar(max));

go

declare @xml XML = '&lt;root&gt;&lt;element1&gt;test&lt;/element1&gt;&lt;element2&gt;test&lt;/element2&gt;&lt;element3&gt;test&lt;/element3&gt;&lt;element4&gt;test&lt;/element4&gt;&lt;element5&gt;test&lt;/element5&gt;&lt;/root&gt;'
    , @x int = 1;

while @x &lt;= 10000
begin
    begin tran

    insert into dbo.t1_XML (col2) values (@xml);
    insert into dbo.t1_VARCHARMAX (col2) values (cast(@xml as varchar(max)));
    insert into dbo.t1_NVARCHARMAX (col2) values (cast(@xml as varchar(max)));

    commit tran

    set @x += 1;
end

exec sp_spaceused 'dbo.t1_XML';
exec sp_spaceused 'dbo.t1_VARCHARMAX';
exec sp_spaceused 'dbo.t1_NVARCHARMAX';
There are two things to know about the XML datatype that together explain what you are experiencing:


As noted in @EvanCarroll's answer, the XML datatype is optimized. Meaning, rather than repeat element and attribute names (which are typically repeated quite a bit and are a large part of why so many people, sometimes rightfully-so, complain about XML documents being so bulky), a dictionary / lookup list is created to store each unique name once, given a numeric ID, and that ID is used to populate the structure of the document. This is why the XML datatype is quite often a better way to store XML documents.
Additionally, the XML datatype uses UTF-16 (Little Endian) to store string values (both element and attribute names as well as any actual string content). This datatype does not use compression, so strings are essentially 2 or 4 bytes per character, with most characters being the 2-byte variety.


Looking at the particular test XML document you are using, and the VARCHAR datatype (1 to 2 bytes per character, most often the 1-byte variety), we can now explain what you are seeing as being a result of:


Each of your elements (root, element1, etc) are used only once, so the only savings of placing the names into the lookup list is to cut the size in exactly half. But, the XML type uses UTF-16 so the size of each string is twice as much, cancelling out the savings of moving the element names into the lookup list. At this point, if only looking at the document structure (i.e. element names) then there is should effectively be no difference between the XML type and the VARCHAR version.
But, the string content in each element (i.e. test) takes up twice the number of bytes: 8 bytes in XML as opposed to 4 bytes in VARCHAR. Given that there are 5 instances of "test" per each row, that is 20 extra bytes per row for the XML type. At 10k rows, that is 200,000 extra bytes of the 600,000 byte difference. The rest is internal overhead of the XML type and the additional page overhead of the additional number of datapages needed to store the same number of rows due to each row being slightly larger.


To better illustrate this behavior, consider the following two variations of XML data: the first being the exact same XML as in the question, and the second being almost the same, but with all elements being the same name. In the second version, all element names are "element1" so that they are the same length as each element in the original version. This is results in the VARCHAR data length being the same in both cases. But the element names being the same in the second version allow the internal optimizations to be more noticeable.

-- Original XML (unique element names -- "element1", "element2", ... "elementN"):
DECLARE @xml XML =  '&lt;root&gt;&lt;element1&gt;test&lt;/element1&gt;&lt;element2&gt;test&lt;/element2&gt;
&lt;element3&gt;test&lt;/element3&gt;&lt;element4&gt;test&lt;/element4&gt;&lt;element5&gt;test&lt;/element5&gt;&lt;/root&gt;';
SELECT DATALENGTH(@xml) AS [XmlBytes],
       DATALENGTH(CONVERT(VARCHAR(MAX), @xml)) AS [VarcharBytes];

-- More "typical" XML (repeated element names -- all "element1"):
DECLARE @xml2 XML = '&lt;root&gt;&lt;element1&gt;test&lt;/element1&gt;&lt;element1&gt;test&lt;/element1&gt;
&lt;element1&gt;test&lt;/element1&gt;&lt;element1&gt;test&lt;/element1&gt;&lt;element1&gt;test&lt;/element1&gt;&lt;/root&gt;';
SELECT DATALENGTH(@xml2) AS [XmlBytes],
       DATALENGTH(CONVERT(VARCHAR(MAX), @xml2)) AS [VarcharBytes];


Results:

ElementNames    XmlBytes    VarcharBytes
------------    --------    ------------
Unique          197         138
Non-Unique      109         138