Jak zaimplementować algorytm oparty na zestawie / UDF

13

Mam algorytm, który muszę uruchomić dla każdego wiersza w tabeli z 800 000 wierszy i 38 kolumnami. Algorytm jest zaimplementowany w języku VBA i wykonuje kilka obliczeń matematycznych przy użyciu wartości z niektórych kolumn do manipulowania innymi kolumnami.

Obecnie używam Excela (ADO) do zapytania SQL i używam VBA z kursorami po stronie klienta, aby zastosować algorytm przez pętlę przez każdy wiersz. Działa, ale uruchomienie zajmuje 7 godzin.

Kod VBA jest na tyle skomplikowany, że przekodowanie go do T-SQL byłoby bardzo pracochłonne.

Przeczytałem o integracji CLR i UDF jako możliwych trasach. Zastanawiałem się także nad umieszczeniem kodu VBA w zadaniu skryptu SSIS, aby zbliżyć się do bazy danych, ale jestem pewien, że istnieje specjalistyczna metodologia tego typu problemów z wydajnością.

Idealnie byłbym w stanie uruchomić algorytm na jak największej liczbie wierszy (wszystkich?) W sposób oparty na zestawie równoległym.

Każda pomoc w znacznym stopniu zależy od tego, jak uzyskać najlepszą wydajność przy tego rodzaju problemach.

--Edytować

Dzięki za komentarze, używam MS SQL 2014 Enterprise, oto kilka szczegółów:

Algorytm znajduje charakterystyczne wzorce w danych szeregów czasowych. Funkcje w ramach algorytmu wykonują wygładzanie wielomianowe, okienkowanie i wyszukują obszary zainteresowania na podstawie kryteriów wejściowych, zwracając tuzin wartości i niektóre wyniki logiczne.

Moje pytanie dotyczy bardziej metodologii niż faktycznego algorytmu: jeśli chcę osiągnąć równoległe obliczenia w wielu wierszach jednocześnie, jakie są moje opcje.

Widzę, że zalecane jest ponowne kodowanie do T-SQL, co jest bardzo pracochłonne, ale możliwe, jednak programista algorytmu działa w VBA i często się zmienia, więc musiałbym być zsynchronizowany z wersją T-SQL i sprawdzać ponownie co zmiana.

Czy T-SQL jest jedynym sposobem na wdrożenie funkcji opartych na zestawie?

medwar19
źródło
3
SSIS może oferować natywną równoległość, zakładając, że dobrze projektujesz przepływ danych. To jest zadanie, którego będziesz szukał, ponieważ musisz wykonać obliczenia wiersz po wierszu. Ale to powiedziawszy, chyba że możesz podać nam szczegóły (schemat, związane z tym obliczenia i co te obliczenia mają nadzieję wykonać), nie możesz pomóc w optymalizacji. Mówią, że pisanie rzeczy w asemblerze może zapewnić najszybszy kod, ale jeśli, podobnie jak ja, będziesz do tego
okropnie
2
Jeśli przetworzysz każdy wiersz niezależnie, możesz podzielić 800 000 wierszy na Npartie i uruchomić Nwystąpienia algorytmu na Nosobnych procesorach / komputerach. Z drugiej strony, jakie jest Twoje główne wąskie gardło - przesyłanie danych z SQL Server do Excela lub rzeczywistych obliczeń? Jeśli zmienisz funkcję VBA, aby natychmiast zwracać jakiś fałszywy wynik, ile czasu zajmie cały proces? Jeśli nadal zajmuje to godziny, wąskie gardło jest w przesyłaniu danych. Jeśli zajmie to kilka sekund, musisz zoptymalizować kod VBA, który wykonuje obliczenia.
Vladimir Baranov
Jest to filtr wywoływany jako procedura składowana: w SELECT AVG([AD_Sensor_Data]) OVER (ORDER BY [RowID] ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING) as 'AD_Sensor_Data' FROM [AD_Points] WHERE [FileID] = @FileID ORDER BY [RowID] ASC Management Studio ta funkcja, która jest wywoływana dla każdego wiersza, zajmuje 50 ms
medwar19
1
Zatem zapytanie, które zajmuje 50 ms i wykonuje się 800 000 razy (11 godzin), zajmuje czas. Czy @FileID jest unikalny dla każdego wiersza, czy też istnieją duplikaty, abyś mógł zminimalizować liczbę potrzebnych zapytań? Możesz również wstępnie obliczyć średnią kroczącą dla wszystkich plików id do tabeli pomostowej za jednym razem (użyj partycji na FileID), a następnie przeszukać tę tabelę bez potrzeby korzystania z funkcji okienkowania dla każdego wiersza. Najlepsza konfiguracja tabeli pomostowej wygląda tak, jak powinna, z włączonym indeksem klastrowym (FileID, RowID).
Mikael Eriksson
1
Najlepiej byłoby, gdybyś w jakiś sposób usunął potrzebę dotykania db dla każdego wiersza. Oznacza to, że musisz albo przejść do TSQL i prawdopodobnie dołączyć do toczącej się średniej kwerendy lub pobrać wystarczającą ilość informacji dla każdego wiersza, więc wszystko, czego potrzebuje algorytm, znajduje się dokładnie w tym wierszu, być może zakodowane w jakiś sposób, jeśli występuje wiele wierszy podrzędnych (xml) .
Mikael Eriksson

Odpowiedzi:

8

Jeśli chodzi o metodologię, wydaje mi się, że szczekasz na niewłaściwe drzewko ;-).

Co wiemy:

Najpierw skonsolidujmy i sprawdźmy, co wiemy o sytuacji:

  • Należy wykonać nieco skomplikowane obliczenia:
    • To musi się zdarzyć w każdym rzędzie tego stołu.
    • Algorytm często się zmienia.
    • Algorytm ... [wykorzystuje] wartości z niektórych kolumn do manipulowania innymi kolumnami
    • Aktualny czas przetwarzania wynosi: 7 godzin
  • Stół:
    • zawiera 800 000 wierszy.
    • ma 38 kolumn.
  • Back-end aplikacji:
  • Baza danych to SQL Server 2014, Enterprise Edition.
  • Dla każdego wiersza wywoływana jest procedura składowana:

    • Uruchomienie zajmuje 50 ms (domyślnie).
    • Zwraca około 4000 wierszy.
    • Definicja (przynajmniej częściowo) to:

      SELECT AVG([AD_Sensor_Data])
                 OVER (ORDER BY [RowID] ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING)
                 as 'AD_Sensor_Data'
      FROM   [AD_Points]
      WHERE  [FileID] = @FileID
      ORDER BY [RowID] ASC

Co możemy założyć:

Następnie możemy spojrzeć na wszystkie te punkty danych razem, aby zobaczyć, czy możemy zsyntetyzować dodatkowe szczegóły, które pomogą nam znaleźć jedno lub więcej szyjek butelek, i albo wskażemy rozwiązanie, albo przynajmniej wykluczymy niektóre możliwe rozwiązania.

Obecny kierunek myślenia w komentarzach jest taki, że głównym problemem jest transfer danych między SQL Server i Excel. Czy to naprawdę tak jest? Jeśli procedura składowana jest wywoływana dla każdego z 800 000 wierszy i zajmuje 50 ms na każde wywołanie (tj. Dla każdego wiersza), daje to 40 000 sekund (nie ms). A to odpowiada 666 minutom (hhmm ;-), czyli nieco ponad 11 godzin. Jednak cały proces miał zająć tylko 7 godzin. Łącznie mamy już 4 godziny, a nawet dodaliśmy czas na wykonanie obliczeń lub zapisanie wyników z powrotem na SQL Server. Więc czegoś tu nie ma.

Patrząc na definicję procedury składowanej, istnieje tylko parametr wejściowy dla @FileID; nie ma żadnego filtra @RowID. Podejrzewam więc, że dzieje się jeden z następujących dwóch scenariuszy:

  • Ta procedura składowana nie jest faktycznie wywoływana dla każdego wiersza, ale zamiast tego dla każdego @FileID, który wydaje się obejmować około 4000 wierszy. Jeśli podane 4000 wierszy zwróconych jest dość spójną kwotą, to tylko 200 z nich grupuje się w 800 000 wierszy. A 200 egzekucji po 50 ms to tylko 10 sekund z 7 godzin.
  • Jeśli ta procedura przechowywana faktycznie zostanie wywołana dla każdego wiersza, to nie przy pierwszym @FileIDprzekazaniu nowego zajmie trochę więcej czasu, aby pobrać nowe wiersze do puli buforów, ale wtedy kolejne 3999 wykonań zwykle powróci szybciej, ponieważ są już w pamięci podręcznej, prawda?

Myślę, że skupienie się na tej procedurze przechowywanej „filtru” lub jakimkolwiek transferze danych z SQL Server do Excela to czerwony śledź .

W tej chwili uważam, że najbardziej odpowiednie wskaźniki słabych wyników to:

  • Istnieje 800 000 wierszy
  • Operacja działa w jednym rzędzie na raz
  • Dane są zapisywane z powrotem do SQL Server, stąd „[używa] wartości z niektórych kolumn do manipulowania innymi kolumnami ” [moja em phas to ;-)]

Podejrzewam, że:

  • chociaż istnieje pole do ulepszeń w zakresie pobierania i obliczania danych, ich ulepszenie nie oznaczałoby znacznego skrócenia czasu przetwarzania.
  • głównym wąskim gardłem jest wydawanie 800 000 oddzielnych UPDATEwyciągów, co stanowi 800 000 oddzielnych transakcji.

Moja rekomendacja (na podstawie obecnie dostępnych informacji):

  1. Twoim największym ulepszeniem byłoby zaktualizowanie wielu wierszy jednocześnie (tj. W jednej transakcji). Powinieneś zaktualizować swój proces, aby działał pod względem każdego FileIDzamiast każdego RowID. Więc:

    1. wczytaj wszystkie 4000 wierszy danego elementu FileIDdo tablicy
    2. tablica powinna zawierać elementy reprezentujące manipulowane pola
    3. przeglądaj tablicę, przetwarzając każdy wiersz tak, jak obecnie
    4. po FileIDobliczeniu wszystkich wierszy w tablicy (tj. dla tego konkretnego ):
      1. rozpocząć transakcję
      2. wywołać każdą aktualizację dla każdego RowID
      3. jeśli nie ma błędów, zatwierdz transakcję
      4. jeśli wystąpił błąd, wycofaj i odpowiednio postępuj
  2. Jeśli indeks klastrowany nie jest jeszcze zdefiniowany jako (FileID, RowID), należy to rozważyć (jak sugerował @MikaelEriksson w komentarzu do pytania). Nie pomoże tym pojedynczym AKTUALIZACJOM, ale przynajmniej nieznacznie poprawi operacje agregujące, takie jak to, co robisz w tej „filtrowanej” procedurze przechowywanej, ponieważ wszystkie są oparte FileID.

  3. Powinieneś rozważyć przeniesienie logiki do skompilowanego języka. Sugerowałbym utworzenie aplikacji .NET WinForms lub nawet aplikacji konsolowej. Wolę aplikację konsolową, ponieważ można ją łatwo zaplanować za pomocą agenta SQL lub zaplanowanych zadań systemu Windows. Nie powinno mieć znaczenia, czy odbywa się to w VB.NET, czy w C #. VB.NET może być bardziej naturalny dla twojego programisty, ale nadal będzie trochę krzywej uczenia się.

    W tej chwili nie widzę powodu, aby przejść do SQLCLR. Jeśli algorytm zmienia się często, denerwujące byłoby zmuszanie do ponownego wdrażania zestawu przez cały czas. Odbudowanie aplikacji konsoli i umieszczenie pliku .exe w odpowiednim folderze współdzielonym w sieci, tak aby po prostu uruchomić ten sam program i zdarza się, że zawsze jest aktualny, powinno być dość łatwe.

    Nie sądzę, aby przeniesienie przetwarzania w pełni do T-SQL pomogłoby, jeśli podejrzewam, że problem jest taki, a ty wykonujesz jedną aktualizację na raz.

  4. Jeśli przetwarzanie zostanie przeniesione do platformy .NET, można następnie użyć parametrów wycenionych w tabeli (TVP), aby przekazać tablicę do procedury UPDATEskładowanej, która wywołałaby wywołanie JOIN do zmiennej tabeli TVP, a zatem jest pojedynczą transakcją . TVP powinien być szybszy niż 4000 INSERTpogrupowane w jedną transakcję. Ale zysk wynikający z używania TVP przez ponad 4000 INSERTs w 1 transakcji prawdopodobnie nie będzie tak znaczący, jak poprawa widoczna przy przejściu z 800 000 oddzielnych transakcji do tylko 200 transakcji po 4000 wierszy każda.

    Opcja TVP nie jest natywnie dostępna dla strony VBA, ale ktoś wymyślił obejście, które może być warte przetestowania:

    Jak poprawić wydajność bazy danych, przechodząc z VBA do SQL Server 2008 R2?

  5. JEŚLI filtr proc używa tylko FileIDw WHEREklauzuli i JEŻELI ten proc jest tak naprawdę wywoływany dla każdego wiersza, możesz zaoszczędzić trochę czasu przetwarzania, buforując wyniki pierwszego uruchomienia i wykorzystując je dla pozostałych wierszy FileID, dobrze?

  6. Po uzyskaniu przetwarzanie odbywa się za fileid , wtedy możemy zacząć mówić o przetwarzaniu równoległym. Ale w tym momencie może to nie być konieczne :). Biorąc pod uwagę, że masz do czynienia z 3 dość dużymi, nie idealnymi częściami: transakcjami Excel, VBA i 800 tys., Każda rozmowa o SSIS lub równoległoboki lub kto-co-wie, to przedwczesna optymalizacja / typ wózka przed koniem . Jeśli uda nam się zmniejszyć ten 7-godzinny proces do 10 minut lub krócej, czy nadal zastanawiasz się nad dodatkowymi sposobami na przyspieszenie? Czy masz na myśli docelowy czas realizacji? Należy pamiętać, że po zakończeniu przetwarzania dla jednego identyfikatora pliku podstawa: jeśli masz aplikację konsoli VB.NET (tj. wiersza polecenia .EXE), nic nie powstrzyma cię przed uruchomieniem kilku tych identyfikatorów plików na raz :), czy to za pośrednictwem kroku CmdExec agenta SQL, czy Zaplanowanych zadań systemu Windows, itp.

I zawsze możesz zastosować podejście „etapowe” i wprowadzić kilka ulepszeń naraz. Na przykład, począwszy od robienia aktualizacji dla, FileIDa więc przy użyciu jednej transakcji dla tej grupy. Następnie sprawdź, czy możesz uruchomić TVP. Następnie zapoznaj się z pobieraniem tego kodu i przenoszeniem go do VB.NET (a TVP działają w .NET, więc ładnie się ładuje).


Czego nie wiemy, co może pomóc:

  • Czy procedura przechowywana „filtrowania” jest uruchamiana dla RowID lub FileID ? Czy w ogóle mamy pełną definicję tej procedury składowanej?
  • Pełny schemat tabeli. Jak szeroki jest ten stół? Ile jest pól o zmiennej długości? Ile pól ma wartość NULLable? Jeśli jakieś są NULLable, ile zawiera NULL?
  • Indeksy dla tej tabeli. Czy jest podzielony na partycje? Czy używana jest kompresja ROW lub PAGE?
  • Jak duża jest ta tabela pod względem MB / GB?
  • Jak obsługiwana jest konserwacja indeksu dla tej tabeli? Jak podzielone są indeksy? Jak aktualne są statystyki?
  • Czy jakieś inne procesy zapisują do tej tabeli podczas trwającego 7 godzin procesu? Możliwe źródło niezgody.
  • Czy jakieś inne procesy zostały odczytane z tej tabeli podczas trwającego 7 godzin procesu? Możliwe źródło niezgody.

AKTUALIZACJA 1:

** Wydaje się, że istnieje pewne zamieszanie dotyczące tego, co VBA (Visual Basic for Applications) i co można z nim zrobić, więc to po prostu upewnienie się, że wszyscy jesteśmy na tej samej stronie internetowej:


AKTUALIZACJA 2:

Jeszcze jedna rzecz do rozważenia: w jaki sposób obsługiwane są połączenia? Czy kod VBA otwiera i zamyka połączenie dla każdej operacji, czy też otwiera połączenie na początku procesu i zamyka pod koniec procesu (tj. 7 godzin później)? Nawet przy pulowaniu połączeń (które domyślnie powinny być włączone dla ADO), nadal powinno istnieć spory wpływ na jednokrotne otwarcie i zamknięcie w przeciwieństwie do otwierania i zamykania 800, 200 lub 1 600 000 razy. Wartości te opierają się na co najmniej 800 000 AKTUALIZACJACH plus 200 lub 800k EXEC (w zależności od tego, jak często faktycznie wykonywana jest procedura przechowywana filtru).

Problem zbyt wielu połączeń jest automatycznie łagodzony przez zalecenie, które przedstawiłem powyżej. Tworząc transakcję i wykonując wszystkie AKTUALIZACJE w ramach tej transakcji, utrzymasz to połączenie otwarte i ponownie wykorzystasz je dla każdej z nich UPDATE. To, czy połączenie jest utrzymywane otwarte od pierwszego wywołania, aby uzyskać 4000 wierszy dla określonego FileID, czy zamknięte po tej operacji „get” i ponownie otwarte dla aktualizacji, ma o wiele mniejszy wpływ, ponieważ teraz mówimy o różnicy Łącznie 200 lub 400 połączeń w całym procesie.

AKTUALIZACJA 3:

Zrobiłem kilka szybkich testów. Należy pamiętać, że jest to test na małą skalę, a nie dokładnie ta sama operacja (czysty INSERT vs EXEC + UPDATE). Różnice w czasie związane ze sposobem obsługi połączeń i transakcji są jednak nadal istotne, dlatego informacje można ekstrapolować, aby mieć tutaj stosunkowo podobny wpływ.

Parametry testu:

  • SQL Server 2012 Developer Edition (64-bit), SP2
  • Stół:

     CREATE TABLE dbo.ManyInserts
     (
        RowID INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
        InsertTime DATETIME NOT NULL DEFAULT (GETDATE()),
        SomeValue BIGINT NULL
     );
  • Operacja:

    INSERT INTO dbo.ManyInserts (SomeValue) VALUES ({LoopIndex * 12});
  • Łączna liczba wkładek na każdy test: 10 000
  • Resetuje się przy każdym teście: TRUNCATE TABLE dbo.ManyInserts;(biorąc pod uwagę charakter tego testu, wykonanie FREEPROCCACHE, FREESYSTEMCACHE i DROPCLEANBUFFERS nie wydawało się dodawać dużej wartości).
  • Model odzyskiwania: SIMPLE (i może 1 GB wolnego miejsca w pliku dziennika)
  • Testy wykorzystujące transakcje używają tylko jednego połączenia, niezależnie od liczby transakcji.

Wyniki:

Test                                   Milliseconds
-------                                ------------
10k INSERTs across 10k Connections     3968 - 4163
10k INSERTs across 1 Connection        3466 - 3654
10k INSERTs across 1 Transaction       1074 - 1086
10k INSERTs across 10 Transactions     1095 - 1169

Jak widać, nawet jeśli połączenie ADO z bazą danych jest już współużytkowane we wszystkich operacjach, zgrupowanie ich w partie przy użyciu jawnej transakcji (obiekt ADO powinien być w stanie to obsłużyć) jest gwarantowane znacznie (tj. Ponad 2x ulepszenie) skrócić całkowity czas procesu.

Solomon Rutzky
źródło
Jest ładne podejście „środkowego człowieka” do tego, co sugeruje srutzky, a mianowicie użycie programu PowerShell w celu uzyskania potrzebnych danych z SQL Server, wywołanie skryptu VBA w celu przetworzenia danych, a następnie wywołanie aktualizacji SP w SQL Server , przekazując klucze i zaktualizowane wartości z powrotem do serwera SQL. W ten sposób łączysz podejście oparte na zestawie z tym, co już masz.
Steve Mangiameli,
@ SteveMangiameli Cześć Steve i dziękuję za komentarz. Odpowiedziałbym wcześniej, ale byłem chory. Jestem ciekawy, jak bardzo różni się twój pomysł od tego, co sugeruję. Wszystko wskazuje na to, że Excel jest nadal wymagany do uruchomienia VBA. A może sugerujesz, że PowerShell zastąpiłby ADO, a jeśli znacznie szybciej na wejściu / wyjściu, byłby tego wart, nawet gdyby po prostu zastąpić tylko wejście / wyjście?
Solomon Rutzky
1
Bez obaw, ciesz się, że czujesz się lepiej. Nie wiem, czy byłoby lepiej. Nie wiemy, czego nie wiemy i dokonałeś świetnej analizy, ale nadal musisz poczynić pewne założenia. We / wy może być wystarczająco znaczące, aby zastąpić je samodzielnie; po prostu nie wiemy. Chciałem tylko przedstawić inne podejście, które może być pomocne w sugerowanych przez ciebie rzeczach.
Steve Mangiameli
@SteveMangiameli Thanks. I dziękuję za wyjaśnienie tego. Nie byłem pewien twojego dokładnego kierunku i uznałem, że najlepiej nie zakładać. Tak, zgadzam się, że posiadanie większej liczby opcji jest lepsze, ponieważ nie wiemy, jakie są ograniczenia dotyczące tego, jakie zmiany można wprowadzić :).
Solomon Rutzky
Hej srutzky, dzięki za szczegółowe przemyślenia! Wcześniej testowałem po stronie SQL, optymalizując indeksy i zapytania i próbując znaleźć wąskie gardła. Zainwestowałem teraz w odpowiedni serwer, 36 rdzeni, dyski SSD PCIe z 1 TB pozbawione dostępu do Internetu. Teraz na wywołanie kodu VB bezpośrednio w SSIS, który wydaje się otwierać wiele wątków dla równoległych wykonań.
medwar19,
2

IMHO i pracując z założenia, że ​​nie jest możliwe ponowne kodowanie subwoofera VBA w SQL, czy zastanawiałeś się nad zezwoleniem skryptowi VBA na zakończenie oceny w pliku Excel, a następnie zapisaniu wyników z powrotem na serwerze SQL za pośrednictwem SSIS?

Mógłbyś zacząć i kończyć subwoofm VBA poprzez odwrócenie wskaźnika albo w obiekcie systemu plików, albo na serwerze (jeśli już skonfigurowałeś połączenie do zapisu z powrotem na serwerze), a następnie użyj wyrażenia SSIS, aby sprawdzić ten wskaźnik dla disablewłaściwość danego zadania w ramach rozwiązania SSIS (tak, aby proces importowania czekał, aż podrzędny VBA zakończy się, jeśli obawiasz się, że przekroczy on harmonogram).

Dodatkowo, możesz mieć skrypt VBA uruchamiany programowo (trochę nieporadny, ale workbook_open()w przeszłości korzystałem z tej właściwości, aby wywoływać zadania tego typu „odpal i zapomnij”).

Jeśli czas oceny skryptu VB zaczyna stanowić problem, można sprawdzić, czy programista VB jest skłonny i zdolny do przeniesienia swojego kodu do zadania skryptu VB w ramach rozwiązania SSIS - z mojego doświadczenia wynika, że ​​aplikacja Excel powoduje duże obciążenie, gdy praca z danymi w tym woluminie.

Peter Vandivier
źródło