środa, czerwiec 29, 2011

Ku pamięci – Jak zaimportować XML do bazy danych

Bawię się ostatnio publicznymi danymi udostępnionymi z serwisu devPytania (info tutaj O pewnym sukcesie) i już zanim przystąpiłem do próby analizy tych danych pojawił się problem.

Dane zostały udostępnione w formacie XML, który oczywiście mimo wszystkich swoich zalet o przenoszalności i dowolnego jego wykorzystania jest średnio zrozumiały przez MS SQL, które to chciałem zaprząc do tejże analizy. Tak więc pierwszą trudnością było to, jak wczytać takie dane do tabel.

Na sieci można znaleźć sporo postów mówiących, jak rozwiązać ten problem ale większość mówi jak XML’a wczytać po prostu do jednej kolumny bez analizy zawartości i stworzenia tylu wierszy w tabeli, ile głównych node’ów mamy w XMLu.
W końcu po prawie godzinie zmarnowanej na próbowaniu tego czy innego rozwiązania udało się znaleźć działający sposób (OPENXML). Prawie idealny.

DECLARE @idoc int
DECLARE @doc varchar(max)
SET @doc ='<xml>'
--Create an internal representation of the XML document.
EXEC sp_xml_preparedocument @idoc OUTPUT, @doc
-- Execute a SELECT statement that uses the OPENXML rowset provider.
INSERT Posts SELECT    *
FROM   OPENXML (@idoc, '/Posts/row',1)
WITH (Id int, PostTypeId int, CreationDate datetime2(7), 
Score int, ViewCount, Body ntext, OwnerUserId int, DeletionDate datetime2(7))

Co tu robimy? Na podstawie zmiennej @doc tworzymy dokument XML a następnie za pomocą OPENXML wybieramy odpowiednie elementy. Parametrami tego polecenia są: dokument XML, XPATH do wzorca wiersza jaki będziemy importować oraz flaga mapowania. Ten ostatni parametr zasługuje na krótkie przyjrzenie się mu. Odpowiada on za ustawienie tego, w jaki sposób będą mapowane dane z XMLa na kolumny w bazie.

0 – domyślnie; takie samo jak 1
1 - dane odczytywane z atrybutów
2 – dane odczytywane z elementów
8 – dane nie będą kopiowane do pola @mp:xmltext, które jest wykorzystywane w celach parsowania

Wartości można łączyć logiczną alternatywą. Czyli wg dokumentacji dozwolone są zarówno wartości 1,2 jak i 3,9 oraz 10.


Po wykonaniu takiego zapytania wiersze z pliku znajdą się w tabeli w bazie danych.

A czemu to prawie idealne rozwiązanie? Z dwóch powodów. Po pierwsze zawartość XML’a trzeba wkleić i podstawić do zmiennej @doc. Nie dałem rady zmusić T-SQL’a do zaczytania pliku i podstawiania wartości do tej zmiennej.
Drugi problem to wielkość danych. varchar to w T-SQL 8000 bajtów, tak więc przy większych plikach (a takowym był plik pytań i odpowiedzi) dane należało podzielić na mniejsze paczki ręcznie. Zmiennej typu text nie da rady utworzyć oczywiście.

Ktoś wie jak można to zrobić lepiej/ładniej/szybciej i ominąć te dwa powody?

18 komentarze:

Maciek pisze...

Cześć,

również potrzebowałem wrzucić dane z XML do bazy danych i wykorzystałem
SMO i SqlBulkCopy. Pisałem o tym na moim blogu:
http://maciej-progtech.blogspot.com/2011/02/smo-podsumowanie-oraz-uzupeniony-kod.html oraz http://maciej-progtech.blogspot.com/2010/06/jak-dataseta-przeniesc-do-mssqla.html. Tam miałem co prawda DataSet'a ale przecież zawsze można takiego wytworzyć.

Pozdrawiam,
Maciek

pawlos pisze...

Tak, taka alternatywa też była, ale chciałem to zrobić z poziomu T-SQL :)

Stwierdziłem, że może nauczę się czegoś nowego.

Paweł

bi pisze...

Spróbuj:
INSERT INTO tabela(xmlData)
SELECT * FROM OPENROWSET(
BULK N'sciezka do pliku.xml',
SINGLE_BLOB) AS x

xmlData to pole typu XML

U mnie to pyka elegancko.

pawlos pisze...

@bi: Ale ja chciałem podzielić XML'a i wrzucić dane do konkretnych kolumn. Nie chciałem mieć jednego pola typu XML. Tj. wartości z node'a Id do kolumny Id, Text do kolumny Text itd.

Paweł

bi pisze...

lepszego rozwiazania zeby wrzucic dane do kolumn nie znam ale w tym przykladzie chodzilo mi o rozwiazanie Twoich 2 niedogodnosci. Napisales ze nie udalo Ci sie zaladowac zrodla dokumentu XML z dysku i ze typ VARCHAR ma za mala pojemnosc i trzeba dzielic.

Z moich testow wynika, ze ponizsze rozwiazuje te 2 problemy? Czy sie myle? :)

DECLARE
@xmlData XML

SELECT
@xmlData = (
SELECT * FROM OPENROWSET(
BULK N'e:\settings.xml',
SINGLE_BLOB) AS x )

SELECT @xmlData

pawlos pisze...

@bi: A dzięki. Sprawdzę to.

Anonimowy pisze...

Panowie,

Polecam Wam wykorzystać nowy typ danych XML (od wersji 2005) i następującą formułę zapytań:

SELECT
[xml].value('@id', 'int'),
[xml].value('@count', 'int')
FROM
@iRooms.nodes('//room') AS [table]([xml])

Jest to proste, miłe i przyjemne. Oczywiście @iRooms to przekazywany przeze mnie XML :)

Pozdrawiam,
Grzegorz Wiśniewski

Anonimowy pisze...

Cóż ja potrzebowałem zaimportować dane z xml do bazy, więc parsuję odpowiednio xml do z góry gotowego datatable i następnie idzie to na SqlBulkCopy, nie chciałem kombinować z poziomu tsql, bo i tak dane najpierw muszą być przetworzone na bardziej "lekkie".

rafek pisze...

A ja jestem ciekawy, czy już są efekty Twojej analizy tych danych? :)

pawlos pisze...

@rafek: Na razie jeszcze nie. Analizuję je i coś tam już tworzę z tych danych, ale zastanawiam się co i jak zaprezentować w analizie :). No i czasu też nie za dużo jest więc to wszystko składa się na brak jakichkolwiek głębszych analiz. Czasem też muszę rozgryza dlaczego np. dane ze zrzutu odbiegają od tego co na stronie.... :)

Paweł

rafek pisze...

@pawlos: to że dane odbiegają od tego co jest na ekranie, jest wynikiem tego, że w zrzucie masz chociażby posty pokasowane.

pawlos pisze...

@rafek: Z postami jest dość logiczne :). Bardziej chodzi mi o rozbieżność w liczbie użytkowników. Już nie pamiętam licz ale licząc na stronie wychodzi 928 użytkowników a w zrzucie było sporo ponad 1000.

rafek pisze...

@pawlos: jest cała masa użytkowników "unregistered", których nie ma na stronie, ale są w bazie.

pawlos pisze...

@rafek: Ok. Jest jakaś możliwość na podstawie danych z zrzutu stwierdzić, które to widma? Nie bardzo chcę aby zaburzały wyniki...

rafek pisze...

@pawlos: myślę, że możesz założyć że wszystkie niezarejestrowane, to widma - użytkownicy typu, wpadli, zapytali i wypadli.

pawlos pisze...

@rafek: Ale w zrzucie chyba nie ma informacji o tym. Są tylko pola: Id, UserTypeId, Reputation, Views. CreationDate no i DisplayName.

rafek pisze...

@pawlos: UserTypeId = 2 to Unregistered.. masz to w UserTypes.xml

pawlos pisze...

@rafek: Ehh...rzeczywiście. Chyba muszę odespać :) Dzięki.