sp_xml_preparedocument: Prepare XML in Memory and Always Release It

sp_xml_preparedocument parses an XML document into memory and gives you a handle to it. OPENXML reads the document through that handle, and sp_xml_removedocument releases it. This post walks through all three steps and keeps the handle safe.

Three steps as boxes: prepare returns a handle, OPENXML reads two rows through it, and release makes the handle unusable with message 8179, plus notes that the handle outlives the batch and that CATCH releases it

A Claim Ticket for Your Document

Think of a coat check. You hand over a coat and get a numbered ticket. The ticket isn’t the coat. It only lets you ask for the coat back. The handle works the same way: it’s an integer that identifies the parsed document in your session.

Keep the value that the procedure returns. Don’t type a number, because the number is not yours to choose.

The Complete Pattern

The block below uses a small library document supplied inline, so it runs in any database. It prepares the document, reads two loans, and releases the handle. The CATCH block releases it if anything fails, and THROW passes the original error on.

DECLARE @Doc xml = N'<Library>
  <Member MemberID="7" Name="Maya">
    <Loan LoanID="101" BookID="12" Days="14"/>
    <Loan LoanID="102" BookID="31" Days="7"/>
  </Member>
</Library>';
DECLARE @Handle int = NULL;
BEGIN TRY
  EXEC sys.sp_xml_preparedocument @Handle OUTPUT, @Doc;
  SELECT LoanID, BookID, Days
  FROM OPENXML(@Handle, '/Library/Member/Loan', 1)
  WITH (LoanID int '@LoanID', BookID int '@BookID', Days int '@Days');
  EXEC sys.sp_xml_removedocument @Handle;
  SET @Handle = NULL;
END TRY
BEGIN CATCH
  IF @Handle IS NOT NULL EXEC sys.sp_xml_removedocument @Handle;
  THROW;
END CATCH;

The query returns loans 101 and 102. After the release, the script sets the handle to NULL, so CATCH can’t release it twice.

Handles Are Not 1

Every call to sp_xml_preparedocument returns its own handle. This script prepares the same document twice in one session and prints both handles.

DECLARE @Doc xml = N'<Library/>';
DECLARE @First int, @Second int;
EXEC sys.sp_xml_preparedocument @First OUTPUT, @Doc;
EXEC sys.sp_xml_preparedocument @Second OUTPUT, @Doc;
SELECT @First AS FirstHandle, @Second AS SecondHandle;
EXEC sys.sp_xml_removedocument @First;
EXEC sys.sp_xml_removedocument @Second;

The numbers came back as 3 and 5, not 1 and 2. Your numbers will differ. Always read the value from the OUTPUT parameter. A large number isn’t a sign of a leak either. In my run the numbers kept climbing, although every document had been released.

A Released Handle Is Dead

After sp_xml_removedocument, the handle means nothing. OPENXML on it fails with message 8179, which says it could not find the prepared statement for that handle.

DECLARE @Handle int;
EXEC sys.sp_xml_preparedocument @Handle OUTPUT, N'<Library><Member MemberID="7"/></Library>';
EXEC sys.sp_xml_removedocument @Handle;
SELECT MemberID FROM OPENXML(@Handle, '/Library/Member', 1) WITH (MemberID int '@MemberID');

The Handle Outlives the Batch

A handle belongs to the session, not the batch. The script below prepares in one batch and stores the number in a temporary table. It reads the document in the next batch. GO ends the first batch, and the handle still works.

CREATE TABLE #Keep (H int);
DECLARE @Handle int;
EXEC sys.sp_xml_preparedocument @Handle OUTPUT, N'<Library><Member MemberID="7" Name="Maya"/></Library>';
INSERT INTO #Keep (H) VALUES (@Handle);
GO

DECLARE @Handle int = (SELECT H FROM #Keep);
SELECT MemberID, Name FROM OPENXML(@Handle, '/Library/Member', 1) WITH (MemberID int '@MemberID', Name varchar(20) '@Name');
EXEC sys.sp_xml_removedocument @Handle;

That’s why the release matters. A prepared document stays in memory until you release it or the session ends. I didn’t measure the memory, so I won’t put a number on it.

Prove That CATCH Releases It

This script fails on purpose with a divide by zero inside the TRY block. CATCH releases the handle. The second batch then tries the same handle.

CREATE TABLE #Seen (H int);
DECLARE @Handle int = NULL;
BEGIN TRY
  EXEC sys.sp_xml_preparedocument @Handle OUTPUT, N'<Library/>';
  INSERT INTO #Seen (H) VALUES (@Handle);
  SELECT 1 / 0 AS Boom;
  EXEC sys.sp_xml_removedocument @Handle;
  SET @Handle = NULL;
END TRY
BEGIN CATCH
  IF @Handle IS NOT NULL EXEC sys.sp_xml_removedocument @Handle;
  SELECT ERROR_NUMBER() AS ErrorNumber;
END CATCH;
GO

DECLARE @Handle int = (SELECT H FROM #Seen);
SELECT localname FROM OPENXML(@Handle, '/Library');

The first batch returns error 8134, and the second returns 8179. The handle is gone, so the CATCH block did its job.

The WITH Clause and the Flags

Without a WITH clause, OPENXML returns the edge table: one row for each node. A nodetype of 1 is an element, 2 is an attribute and 3 is text. The WITH clause gives you typed columns instead.

DECLARE @Handle int;
EXEC sys.sp_xml_preparedocument @Handle OUTPUT, N'<Library><Member MemberID="7"><Loan LoanID="101"/></Member></Library>';
SELECT id, parentid, nodetype, localname, text FROM OPENXML(@Handle, '/Library/Member/Loan');
EXEC sys.sp_xml_removedocument @Handle;

The third argument of OPENXML is the flags value. Flags 1 maps columns to attributes and flags 2 maps them to child elements. With the wrong flag, the columns come back NULL, as the first row below shows.

DECLARE @Handle int;
EXEC sys.sp_xml_preparedocument @Handle OUTPUT, N'<Library><Loan><LoanID>101</LoanID><Days>14</Days></Loan></Library>';
SELECT 'flags 1' AS Setting, LoanID, Days FROM OPENXML(@Handle, '/Library/Loan', 1) WITH (LoanID int, Days int)
UNION ALL
SELECT 'flags 2', LoanID, Days FROM OPENXML(@Handle, '/Library/Loan', 2) WITH (LoanID int, Days int);
EXEC sys.sp_xml_removedocument @Handle;

Three result grids: typed loan rows with a WITH clause, the edge table with node types 1, 2 and 3 for element, attribute and text, and the flags test where flags 1 returns NULL and flags 2 returns 101 and 14

Clean Up

The two temporary tables disappear when the session ends. Drop them now if you want to keep the session tidy.

DROP TABLE #Keep;
DROP TABLE #Seen;

What to Remember

Prepare, read, release. Keep the handle in a variable. Release it in the success path and in the CATCH block. Never reuse it afterward.

To go further, read XML Document Could Not Be Created Because Server Memory Is Low, XML nodes: Turn Repeated Elements Into Rows and Rethrowing Errors With THROW and Keeping the Original Number.

Quick reference card: sp_xml_preparedocument

An XML handle is not the document, it is a ticket you have to hand back.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

SQL Memory, SQL Scripts, SQL Stored Procedure, SQL XML
Previous Post
Shredding XML in SQL Server: Turn XML Back Into Rows
Next Post
OPENXML Rowpattern in SQL Server: Search Every Level and Read the Parent

Related Posts

2 Comments. Leave new

  • The correct option for the above question is Option 1)
    The handle as an INT

    Explanation
    Reads the Extensible Markup Language (XML) text provided as input, then parses the text using the MSXML parser (Msxml2.dll), and provides the parsed document in a state ready for consumption. This parsed document is a tree representation of the various nodes (elements, attributes, text, comments, and so on) in the XML document.

    sp_xml_preparedocument returns a handle that can be used to access the newly created internal representation of the XML document. This handle is valid for the duration of the connection to Microsoft® SQL Server™ 2000, until the connection is reset, or until the handle is invalidated by executing sp_xml_removedocument.

    Note A parsed document is stored in the internal cache of SQL Server 2000. The MSXML parser uses one-eighth the total memory available for SQL Server. To avoid running out of memory, run sp_xml_removedocument to free up the memory.

    Syntax

    sp_xml_preparedocument hdoc OUTPUT
    [, xmltext]
    [, xpath_namespaces]

    Arguments

    hdoc

    Is the handle to the newly created document. hdoc is an integer.

    [xmltext]

    Is the original XML document. The MSXML parser parses this XML document. xmltext is a text (char, nchar, varchar, nvarchar, text, or ntext) parameter. The default value is NULL, in which case an internal representation of an empty XML document is created.

    [xpath_namespaces]

    Specifies the namespace declarations that are used in row and column XPath expressions in OPENXML. The default value is .
    xpath_namespaces provides the namespace URIs for the prefixes used in the XPath expressions in OPENXML by means of a well-formed XML document. xpath_namespaces declares the prefix that must be used to refer to the namespace urn:schemas-microsoft-com:xml-metaprop, which provides meta data about the parsed XML elements. Although you can redefine the namespace prefix for the metaproperty namespace using this technique, this namespace is not lost. The prefix mp is still valid for urn:schemas-microsoft-com:xml-metaprop even if xpath_namespaces contains no such declaration. xpath_namespaces is a text (char, nchar, varchar, nvarchar, text, or ntext) parameter.

    Dilip Kumar Jena
    Country : INDIA

    Reply
  • I ran the below command many times:

    [quote]
    Declare @hdoc int
    exec sp_xml_preparedocument @hdoc output, @xmlFile

    select @hdoc
    [/quote]

    Resulted in the value of @hdoc variable has been set to 72…
    Can you please tell me how can I reset it?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.