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:
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
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ł
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.
@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ł
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
@bi: A dzięki. Sprawdzę to.
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
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".
A ja jestem ciekawy, czy już są efekty Twojej analizy tych danych? :)
@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ł
@pawlos: to że dane odbiegają od tego co jest na ekranie, jest wynikiem tego, że w zrzucie masz chociażby posty pokasowane.
@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.
@pawlos: jest cała masa użytkowników "unregistered", których nie ma na stronie, ale są w bazie.
@rafek: Ok. Jest jakaś możliwość na podstawie danych z zrzutu stwierdzić, które to widma? Nie bardzo chcę aby zaburzały wyniki...
@pawlos: myślę, że możesz założyć że wszystkie niezarejestrowane, to widma - użytkownicy typu, wpadli, zapytali i wypadli.
@rafek: Ale w zrzucie chyba nie ma informacji o tym. Są tylko pola: Id, UserTypeId, Reputation, Views. CreationDate no i DisplayName.
@pawlos: UserTypeId = 2 to Unregistered.. masz to w UserTypes.xml
@rafek: Ehh...rzeczywiście. Chyba muszę odespać :) Dzięki.
Prześlij komentarz