none
Query wird extrem langsam, sobald TOP N im Spiel ist RRS feed

  • Frage

  • Hallo zusammen,

    Ich habe hier eine Anforderung eine Query zu bauen um herauszufinden welche als Pflicht deklarierte Software auf welchen Server NICHT installiert ist.

    Als Datenbasis habe ich 3 Tabellen.

    1. Die Liste aller vorhandenen Server
    2. Die Liste der Software Installationen. Hier ist für jede gefundene Software eine Zeile pro Server enthalten.
    3. Eine manuell gepflegte Liste an SW-Paketen nach denen gesucht werden soll. Die Einträge können auch Wildcards enthalten.

    Die überschneidende Spalte zwischen 1. und 2. Tabelle ist der Servername.

    Wir reden hier von Datenmengen von 

    1. Ca. 2.000 Zeilen
    2. Ca. 110.000 Zeilen
    3. Derzeit nur 1 Zeile

    Führe ich diese Query aus oder mach ein Select * von der neuen View ist die in unter einer Sekunde durch und liefert ca. 360 Rows.
    Leider arbeitet unser Webfrontend aber gerne mit TOP N für die Pagination und das bricht mir gerade das Genick.

    Sobald Top N im Spiel ist scheint das überhaupt nicht mehr fertig zu werden. Meistens brech ich so nach 3-4minuten ab. Das Webfrontend würde wohl nach 20 Sekunden aufgeben.

    Hättet ihr Ideen wie ich das besser machen kann?
    Plan B wäre es die View asynchron in eine Table zu materialisieren. Aber mehr Schahm hätte es schon, wenn das alles mit Live-Daten funktionieren würde.

    ALTER view View_Device_Server_Win_Missing_Required_SW
    AS
    
    -- 1. CTE erstellt eine Namensliste aus manueller Searchliste und realer SW-Installations-Liste
    WITH cte_Required_SWPackages
    AS (
    	SELECT DISTINCT Installed.Displayname
    		,Installed.[Version]
    	FROM p_Device_Software_Installations Installed
    	INNER JOIN CFGRequiredSoftware Req ON (
    			Installed.Displayname LIKE Req.Displayname
    			AND Installed.[Version] LIKE (
    				CASE 
    					WHEN Req.[Version] IS NULL
    						THEN '%'
    					WHEN Req.[Version] = ''
    						THEN '%'
    					ELSE Req.[Version]
    					END
    				)
    			)
    	)
    	-- 2. CTE filtert nur die "Aktive" Server raus
    	,cte_Active_Windows_Servers
    AS (
    	SELECT [Name]
    		,Verw
    		,DeviceType
    		,id
    	FROM View_Device_Server_Win
    	WHERE InstallStatus = 'active'
    	)
    
    	-- 3. CTE filtert nur gesuchte Software/Server-Zeilen aus der Gesamt Tabelle
    	,cte_Currently_Installed
    AS (
    	SELECT p_Device_Software_Installations.Computername
    		,p_Device_Software_Installations.DisplayName
    		,p_Device_Software_Installations.[Version]
    	FROM p_Device_Software_Installations
    	WHERE p_Device_Software_Installations.DisplayName IN (
    			SELECT Displayname
    			FROM cte_Required_SWPackages
    			)
    		AND p_Device_Software_Installations.[Version] IN (
    			SELECT [Version]
    			FROM cte_Required_SWPackages
    			)
    	)
    	-- 4. CTE baut ein Kartesisches Produkt aus Active-Serverlist und gesuchter Softwareprodukte
    	,cte_workinglist
    AS (
    	SELECT *
    	FROM cte_Required_SWPackages
    	FULL JOIN cte_Active_Windows_Servers ON (0 = 0)
    	)
    
    -- Finaler Query stellt das Kartesische Produkt gegenüber der reduzierten SW-Installliste cte_Currently_Installed um raus zufinden was nur im K-Produkt enthalten ist
    SELECT cte_workinglist.[Name]
    	,cte_workinglist.[Version]
    	,cte_workinglist.DisplayName
    	,cte_workinglist.Verw
    	,cte_workinglist.DeviceType
    	,cte_workinglist.id
    FROM cte_workinglist
    LEFT JOIN cte_Currently_Installed ON (cte_workinglist.[Name] = cte_Currently_Installed.Computername AND cte_workinglist.DisplayName = cte_Currently_Installed.DisplayName AND cte_workinglist.[Version] = cte_Currently_Installed.[Version])
    WHERE Computername IS NULL

    Donnerstag, 26. November 2020 07:32

Antworten

  • Nun ja, ein SQL ist dann am schnellsten, wenn erst mal alles abgerufen und dann sortiert wird.
    Wobei der Sort im Programm dann zu empfehlen ist (die meisten Programmiersprachen können das) damit SQL nicht prüfen muss ob es einen passenden Index gibt.

    Bei TOP N muss erst mal alles im Speicher geladen und sortiert werden um dann den Rest wieder zu verwerfen.
    Da kann es schon mal andere Optimierungswege geben.

    Was den Inner Join angeht, so gilt hier einfach:

    select * from a inner join b on a.key = b.key

    ist identisch zu

    select * from b inner join a on a.key = b.key

    Aus eine 1:N-Beziehung (N=1 ist auch möglich) wird eine N:1-Beziehung.
    Ich kenne zumindest eine Datenbank, die im Zuge der SQL-Optimierung einen SQL diesbezüglich auch anpasst.

    Wichtig ist für deinen "where Exists" ob die Trefferqoute in Tabelle A oder in Tabelle B besser einzuschränken ist.
    Da hier durch Exists auf den Inner Join auf beide Tabellen Bezug genommen wird, sind durch Umdrehen des Joins u.U. weniger Zugriffe zu erwarten.

    Eine Optimierung von Minuten zu Sekunden finde ich aber schon gut, wenn man bedenkt mit welchen simplen Maßnahmen dies erreicht werden kann.

    Statt View kann man ggf. auch eine Table-Function verwenden:
    https://www.sqlservertutorial.net/sql-server-user-defined-functions/sql-server-table-valued-functions/

    Hier kannst du tatsächlich z.B. mittels TEMPORARY TABLE die With-Zwischentabellen erstellen und den finalen Select aus den nun tatsächlichen Tabellen zurück geben.
    http://www.sqlines.com/articles/sql-server/local_and_global_temporary_tables

    Solange eine Connection/Sitzung stabil bleibt, kann man der Prozedur auch z.B. die Pagingwerte als Parameter mitgeben und braucht nur die fertige Tabelle, die für die Sitzung bestehen bleibt, immer wieder auszulesen.

    • Als Antwort markiert Manuel Aigner Freitag, 27. November 2020 09:55
    Donnerstag, 26. November 2020 13:44
  • Statt "select distinct" solltest du einen "select a, b .... group by a, b" verwenden, da hier ein Index (falls vorhanden) verwendet werden kann.

    Warum verwendest du "Installed.Displayname LIKE Req.Displayname" statt "Installed.Displayname = Req.Displayname?
    Dies lässt ebenso keinen Index zu. Like lohnt sich nur, wenn du auch tatsächlich "%" im Suchbegriff hast.

    Dies gilt auch für die 2. Bedingung:

    and (Installed.[Version] = Req.[Version] or Req.[Version] = '' or Req.[Version] is null)

    Wozu machst dsu einen FULL JOIN?
    Dieser liefert ja alle Daten der rechten Seite unabhängig vom Ergebnis der linken Seite.
    Ist da ggf. ein CROSS JOIN nicht der richtigere Weg?

    Dann noch zum Verständnis für dich:
    Eine CTE ist keine temporäre Tabelle sondern jeder Zugriff auf eine CTE-Tabelle führt zur Neuberechnung des Ergebnisses. Wenn also keine Indizes verwendet werden können ergeben sich schnell Millionen von Zugriffen.
    Dein "... in ... cte_Required_SWPackages" führt den Distinct je Zeile erneut aus.

    Eine INNER JOIN lässt sich auch umdrehen da ja nur Kombinationen aus beiden benötigt werden.
    Statt eines "in (select ...)" ist ein "exists (Select ... where )" erheblich schneller, ins besonders wenn ein Index möglich ist. Somit kannst du die 2 " in ..." zu einem "exists" vereinen.

    Die Anzahl der Zugriffe wird sich erheblich reduzieren.


    • Bearbeitet Der Suchende Donnerstag, 26. November 2020 09:02
    • Als Antwort markiert Manuel Aigner Donnerstag, 26. November 2020 10:27
    Donnerstag, 26. November 2020 09:01

Alle Antworten

  • Statt "select distinct" solltest du einen "select a, b .... group by a, b" verwenden, da hier ein Index (falls vorhanden) verwendet werden kann.

    Warum verwendest du "Installed.Displayname LIKE Req.Displayname" statt "Installed.Displayname = Req.Displayname?
    Dies lässt ebenso keinen Index zu. Like lohnt sich nur, wenn du auch tatsächlich "%" im Suchbegriff hast.

    Dies gilt auch für die 2. Bedingung:

    and (Installed.[Version] = Req.[Version] or Req.[Version] = '' or Req.[Version] is null)

    Wozu machst dsu einen FULL JOIN?
    Dieser liefert ja alle Daten der rechten Seite unabhängig vom Ergebnis der linken Seite.
    Ist da ggf. ein CROSS JOIN nicht der richtigere Weg?

    Dann noch zum Verständnis für dich:
    Eine CTE ist keine temporäre Tabelle sondern jeder Zugriff auf eine CTE-Tabelle führt zur Neuberechnung des Ergebnisses. Wenn also keine Indizes verwendet werden können ergeben sich schnell Millionen von Zugriffen.
    Dein "... in ... cte_Required_SWPackages" führt den Distinct je Zeile erneut aus.

    Eine INNER JOIN lässt sich auch umdrehen da ja nur Kombinationen aus beiden benötigt werden.
    Statt eines "in (select ...)" ist ein "exists (Select ... where )" erheblich schneller, ins besonders wenn ein Index möglich ist. Somit kannst du die 2 " in ..." zu einem "exists" vereinen.

    Die Anzahl der Zugriffe wird sich erheblich reduzieren.


    • Bearbeitet Der Suchende Donnerstag, 26. November 2020 09:02
    • Als Antwort markiert Manuel Aigner Donnerstag, 26. November 2020 10:27
    Donnerstag, 26. November 2020 09:01
  • Vielen Dank schon mal für deine Einwürfe. 

    "Warum verwendest du "Installed.Displayname LIKE Req.Displayname" statt "Installed.Displayname = Req.Displayname?

    Dies lässt ebenso keinen Index zu. Like lohnt sich nur, wenn du auch tatsächlich "%" im Suchbegriff hast."

    Das mache ich weil in der Search-Tabelle auch Wildcards drinnen sein können. Sowohl beim DisplayName als auch der Version. Leider ist auf DisplayName eh kein Index möglich. Die wurde damals schlauerweise mit nvarchar(Max) deklariert.

    Die groub by Geschichte anstatt von Distinct hab ich umgesetzt.

    Wozu machst dsu einen FULL JOIN?
    Dieser liefert ja alle Daten der rechten Seite unabhängig vom Ergebnis der linken Seite.
    Ist da ggf. ein CROSS JOIN nicht der richtigere Weg?

    Vollkommen richtig. Hab ich auch umgesetzt. Ich war gestern nach nem Tag gebastle an der Query schon etwas Banane

    Dann noch zum Verständnis für dich:
    Eine CTE ist keine temporäre Tabelle sondern jeder Zugriff auf eine CTE-Tabelle führt zur Neuberechnung des Ergebnisses. Wenn also keine Indizes verwendet werden können ergeben sich schnell Millionen von Zugriffen.

    Das wusste ich in der Tat nicht. Dachte eigentlich, dass die CTE-Ergebnisse im RAM geladen werden und dann immer wieder verwendet werden können ohne extra Rechenaufwand. In dem Fall wäre wohl ne Temptable sinnvoller. Das geht aber leider in ner View nicht, welche ich benötige um damit im Web arbeiten zu können (Laravel (PHP) Framework / Datatables Modul).
    Gibt es da noch irgend einen Trick den ich noch nicht kenne, wie man das gescheiter umsetzen kann und was View-Kompatibel ist?

    Eine INNER JOIN lässt sich auch umdrehen da ja nur Kombinationen aus beiden benötigt werden.

    Das versteh ich jetzt leider nicht

    Statt eines "in (select ...)" ist ein "exists (Select ... where )" erheblich schneller, ins besonders wenn ein Index möglich ist. Somit kannst du die 2 " in ..." zu einem "exists" vereinen.

    gute Idee, hab ich jetzt auch so umgesetzt.

    Nach den ganzen Maßnahmen ist die Query (View) noch immer nicht schnell, sobald man TOP N verwendet, aber es wird zumindest schon etwas erträglicher. Für TOP 200 aus jetzt 11000 Rows dauerts jetzt ca. 7 Sekunden. vorher lief es mehrere Minuten. Ich versteh auch noch immer nicht, warum mir der TOP N die Performance so dermaßen killt. Die 11000 Zeilen kommen ohne TOP N binnen < 1Sekunde zurück.

    Donnerstag, 26. November 2020 10:25
  • Hallo Manuel,

    ich würde mal ROW_NUMBER() anstelle von TOP probieren.

    DECLARE @Source TABLE ( ID int, Name varchar( 255 ) );
    
    INSERT INTO @Source ( ID, Name )
    VALUES ( 1, 'Name 1' ), ( 2, 'Name 2' ), ( 3, 'Name 3' ), ( 4, 'Name 5' );
    
    WITH Query AS
    (
        SELECT ID,
               Name,
               ROW_NUMBER() OVER( ORDER BY ID ) AS RowNumber
        FROM   @Source
    )
    SELECT ID,
           Name
    FROM   Query
    WHERE  RowNumber <= 2

    D.h. in deiner letzten Abfrage deiner View nimmst Du die ROW_NUMBER() mit und kannst dann von außen danach filtern.


    Gruß, Stefan
    Microsoft MVP - Visual Developer ASP/ASP.NET (2001-2018)
    https://www.asp-solutions.de/ - IT Beratung, Softwareentwicklung, Remotesupport



    Donnerstag, 26. November 2020 11:23
    Moderator
  • Hi Stefan,

    Jap, genau so macht das Laravel auch ab Seite 2 der pagination. Die erste Seite macht es aber immer mit TOP N. Da das im Framework so gegeben ist, ist es auch gar nicht so einfach zu umgehen.

    Man gibt einfach an wel

    Hi Stefan,

    Jap, genadddu so macht das Laravel auch ab Seite 2 der pagination. Die erste Seite macht es aber immer mit TOP N. Da das im Framework so gegeben ist, ist es auch gar nicht so einfach zu umgehen.

    Man gibt einfach an welche Table oder View man abfragen will. Kann noch die Sort Columns + Richtung und generell welche Columns selektiert werden sollen definieren und den Query dahinter macht es dann von selbst. 

    Wenn man dann so eine Table im Web ansehen will passieren im Wesentlichen folgende 2 Queries:

    1. Select count(*) from <mytable2query>
    Das braucht es oben für den RowCounter um auszugeben wieviele Rows von wie vielen Rows gesamt angezeigt werden.

    2. Anzeige der 1. Page: Select TOP <Seitengröße dynamisch nach responsive Design> <myColumns,...> from <mytable2query>
    2a. Jede weitere Page läuft nach dem Konzept wie Stefan es schrieb.

    An dieser Methodik kommt man nicht rum, sofern man nicht am Framework vorbei alles neu erfinden will.

    che Table oder View man abfragen will. Kann noch die Sort Columns + Richtung und generell welche Columns selektiert werden sollen definieren und den Query dahinter macht es dann von selbst. 

    Wenn man dann so eine Table im Web ansehen will passieren im Wesentlichen folgende 2 Queries:

    1. Select count(*) from <mytable2query>
    Das braucht es oben für den RowCounter um auszugeben wieviele Rows von wie vielen Rows gesamt angezeigt werden.

    2. Anzeige der 1. Page: Select TOP <Seitengröße dynamisch nach responsive Design> <myColumns,...> from <mytable2query>
    2a. Jede weitere Page läuft nach dem Konzept wie Stefan es schrieb.

    An dieser Methodik kommt man nicht rum, sofern man nicht am Framework vorbei alles neu erfinden will.

    EDIT:

    Also mir bricht definitiv die erste cte_Required_SWPackages das Genick. Die läuft mit TOP N gewaltige 111.000 mal durch. Das ergibt dann auf die beiden Joined-Tables dann nen Zugriffscount im 3-Stelligen Mio. Bereich. Der Querie läuft so mit TOP 100 über die View gut 16 Min.

    Ohne TOP N macht sie das nicht und ist in 500ms durch. Auch wenn ich Filter darauf anwende ohne Top ist das Pfeil schnell.

    Ich verstehs nicht warum da der Queryoptimizer so komisch arbeitet. Kann der nicht einfach das Gesamtergebnis berechnen lassen und dann im RAM durch TOP N nach N Rows nen Truncate machen?!

    • Bearbeitet Manuel Aigner Donnerstag, 26. November 2020 13:36 Erweiterung
    Donnerstag, 26. November 2020 12:34
  • Nun ja, ein SQL ist dann am schnellsten, wenn erst mal alles abgerufen und dann sortiert wird.
    Wobei der Sort im Programm dann zu empfehlen ist (die meisten Programmiersprachen können das) damit SQL nicht prüfen muss ob es einen passenden Index gibt.

    Bei TOP N muss erst mal alles im Speicher geladen und sortiert werden um dann den Rest wieder zu verwerfen.
    Da kann es schon mal andere Optimierungswege geben.

    Was den Inner Join angeht, so gilt hier einfach:

    select * from a inner join b on a.key = b.key

    ist identisch zu

    select * from b inner join a on a.key = b.key

    Aus eine 1:N-Beziehung (N=1 ist auch möglich) wird eine N:1-Beziehung.
    Ich kenne zumindest eine Datenbank, die im Zuge der SQL-Optimierung einen SQL diesbezüglich auch anpasst.

    Wichtig ist für deinen "where Exists" ob die Trefferqoute in Tabelle A oder in Tabelle B besser einzuschränken ist.
    Da hier durch Exists auf den Inner Join auf beide Tabellen Bezug genommen wird, sind durch Umdrehen des Joins u.U. weniger Zugriffe zu erwarten.

    Eine Optimierung von Minuten zu Sekunden finde ich aber schon gut, wenn man bedenkt mit welchen simplen Maßnahmen dies erreicht werden kann.

    Statt View kann man ggf. auch eine Table-Function verwenden:
    https://www.sqlservertutorial.net/sql-server-user-defined-functions/sql-server-table-valued-functions/

    Hier kannst du tatsächlich z.B. mittels TEMPORARY TABLE die With-Zwischentabellen erstellen und den finalen Select aus den nun tatsächlichen Tabellen zurück geben.
    http://www.sqlines.com/articles/sql-server/local_and_global_temporary_tables

    Solange eine Connection/Sitzung stabil bleibt, kann man der Prozedur auch z.B. die Pagingwerte als Parameter mitgeben und braucht nur die fertige Tabelle, die für die Sitzung bestehen bleibt, immer wieder auszulesen.

    • Als Antwort markiert Manuel Aigner Freitag, 27. November 2020 09:55
    Donnerstag, 26. November 2020 13:44
  • Hallo Manuel,

    Jap, genau so macht das Laravel auch ab Seite 2 der pagination. Die erste Seite macht es aber immer mit TOP N. Da das im Framework so gegeben ist, ist es auch gar nicht so einfach zu umgehen.

    dann würde ich sagen, dass das Framework was verkehrt macht und man sollte dem Hersteller sagen, dass er das entweder bitte korrigiert oder bspw. über eine Konfigurationseinstellung so anpassen kann, dass man entweder immer mit ROW_NUMBER(), OFFSET ... FETCH oder eben mit TOP arbeitet (wobei ich TOP  ehrlich gesagt für veraltet und daher nicht mehr sinnvoll einsetzbar halte, zumindest in SQL Server Versionen, die ROW_NUMBER() und/oder OFFSET ... FETCH unterstützen.


    Gruß, Stefan
    Microsoft MVP - Visual Developer ASP/ASP.NET (2001-2018)
    https://www.asp-solutions.de/ - IT Beratung, Softwareentwicklung, Remotesupport


    Donnerstag, 26. November 2020 14:42
    Moderator
  • Von der Performance sollte "where RowNumber ..." und Skip/Top (bzw. offset/fetch) annähernd gleichwertig sein, da auch bei Row_Number() eine Sortierung, i.d.R. nach Name statt Id, über das Gesamtergebnis sowieso erforderlich ist.
    Ich sortiere ja nicht nach internen unbekannten ID's sondern nach den Feldinhalten.

    Außerdem müsste ja die RowNumber mit "between N and M" je Seite gefiltert werden.

    • Bearbeitet Der Suchende Donnerstag, 26. November 2020 15:31
    Donnerstag, 26. November 2020 15:19
  • Ihr seid die Besten :)

    Der Hint mit der Table-Values UDF war Goldes Wert. Hab ich gleich mal so umgesetzt. Funktioniert auch schon mal ganz gut, wenns auch sicher noch dort und da Optimierungspotenzial gibt.

    Jetzt gibts aber noch ne Zusatzaufgabe. Man soll sehen können ob eine bestimmte Software überhaupt nicht vorhanden ist, in egal welcher Version. Das hätte ich jetzt mit nem Left Join ON LIKE (ich weiß, Performance lässt grüßen) versucht. Aber das funktioniert nicht wie erwartet. Egal wie ich den Wildcard % definiere. Der wird einfach gefressen. Es wird nur gefunden was ganz genau so in meiner Steuertabelle Required-SWPackages steht.

    ALTER FUNCTION udf_Device_Server_Win_Missing_Required_SW (@Mode NVARCHAR(32))
    RETURNS @MissingSW TABLE (
    	[Name] NVARCHAR(64)
    	,DisplayName NVARCHAR(256)
    	,[Version] NVARCHAR(64)
    	,Verw NVARCHAR(16)
    	,PatchAm NVarchar(255)
    	,DeviceType NVARCHAR(128)
    	,id INT
    	,Computername nvarchar(64)
    	,DisplayName2 nvarchar(256)
    	)
    AS
    BEGIN
    	-- Find Softwarepackages
    	DECLARE @Required_SWPackages TABLE (
    		Displayname NVARCHAR(256)
    		,[Version] NVARCHAR(64)
    		)
    
    	IF @mode = 'allversions'
    		INSERT INTO @Required_SWPackages
    		SELECT Installed.Displayname
    			,Installed.[Version]
    		FROM p_Device_Software_Installations Installed
    		INNER JOIN CFGRequiredSoftware Req ON (
    				Installed.Displayname LIKE Req.Displayname
    				AND Installed.[Version] LIKE (
    					CASE 
    						WHEN Req.[Version] IS NULL
    							THEN '%'
    						WHEN Req.[Version] = ''
    							THEN '%'
    						ELSE Req.[Version]
    						END
    					)
    				)
    		GROUP BY Installed.Displayname
    			,Installed.[Version]
    	ELSE
    		INSERT INTO @Required_SWPackages
    		SELECT Displayname
    			,[Version]
    		FROM CFGRequiredSoftware
    
    	-- Filter Active Servers
    	DECLARE @Active_Windows_Servers TABLE (
    		[Name] NVARCHAR(64)
    		,Verw NVARCHAR(16)
    		,PatchAm NVARCHAR(255)
    		,DeviceType NVARCHAR(128)
    		,id INT
    		)
    
    	INSERT INTO @Active_Windows_Servers
    	SELECT [Name]
    		,Verw
    		,PatchAm
    		,DeviceType
    		,id
    	FROM View_Device_Server_Win
    	WHERE InstallStatus = 'active'
    
    	-- Get currently installed (filtered by Required_SWPackages)
    	DECLARE @Currently_Installed TABLE (
    		Computername NVARCHAR(64)
    		,DisplayName NVARCHAR(256)
    		,[Version] NVARCHAR(64)
    		)
    
    	INSERT INTO @Currently_Installed
    	SELECT p_Device_Software_Installations.Computername
    		,p_Device_Software_Installations.DisplayName
    		,p_Device_Software_Installations.[Version]
    	FROM p_Device_Software_Installations
    	WHERE EXISTS (
    			SELECT Displayname
    			FROM @Required_SWPackages
    			WHERE DisplayName = p_Device_Software_Installations.DisplayName
    				AND [Version] = p_Device_Software_Installations.[Version]
    			)
    
    	--- Creating cardesian product of active servers and required SW
    	DECLARE @Workinglist TABLE (
    		DisplayName NVARCHAR(256)
    		,[Version] NVARCHAR(64)
    		,[Name] NVARCHAR(64)
    		,Verw NVARCHAR(16)
    		,PatchAm NVARCHAR(255)
    		,DeviceType NVARCHAR(128)
    		,id INT
    		)
    
    	INSERT INTO @Workinglist
    	SELECT *
    	FROM @Required_SWPackages SW
    	CROSS JOIN @Active_Windows_Servers MyServers
    
    
    	-- Executing final Return Query
    	IF @mode = 'allversions'
    		INSERT INTO @MissingSW
    		SELECT workinglist.[Name]
    			,workinglist.DisplayName
    			,workinglist.[Version]
    			,workinglist.Verw
    			,workinglist.PatchAm
    			,workinglist.DeviceType
    			,workinglist.id
    			,Currently_Installed.Computername
    			,Currently_Installed.DisplayName
    		FROM @Workinglist workinglist
    		LEFT JOIN @Currently_Installed Currently_Installed ON (
    				workinglist.[Name] = Currently_Installed.Computername
    				AND workinglist.DisplayName = Currently_Installed.DisplayName
    				AND workinglist.[Version] = Currently_Installed.[Version]
    				)
    		WHERE Computername IS NULL
    		ORDER BY Name
    	ELSE
    		INSERT INTO @MissingSW
    		SELECT workinglist.[Name]
    			,workinglist.DisplayName
    			,workinglist.[Version]
    			,workinglist.Verw
    			,workinglist.PatchAm
    			,workinglist.DeviceType
    			,workinglist.id
    			,Currently_Installed.Computername
    			,Currently_Installed.DisplayName
    		FROM @Workinglist workinglist
    		LEFT JOIN @Currently_Installed Currently_Installed ON (workinglist.[Name] = Currently_Installed.Computername and (Currently_Installed.DisplayName Like Convert(nvarchar(64),workinglist.DisplayName)+'%'))
    		--WHERE Computername IS NULL
    		ORDER BY Name
    
    	RETURN;
    END;

    Freitag, 27. November 2020 08:16
  • Hallo Manuel,

    ich würde die Statements mal auf einzelne Abfragen runterbrechen und dann gezielt schauen, wo es hakt. Bei einer komplexen Abfrage über zig Ebenen/Unterabfragen wird es schwer, das zu debuggen.

    Dazu kommt, dass wir weder deine Tabellenstruktur noch deine Daten kennen. Poste daher bitte CREATE TABLE Statements für alle beteiligten Tabellen, INSERT INTO Statements für Beispieldaten und dazu das gewünschte Ergebnis, basierend auf exakt diesen Beispieldaten.


    Gruß, Stefan
    Microsoft MVP - Visual Developer ASP/ASP.NET (2001-2018)
    https://www.asp-solutions.de/ - IT Beratung, Softwareentwicklung, Remotesupport

    Freitag, 27. November 2020 09:02
    Moderator
  • Der Leftjoin kann seinen Status LEFT verlieren, wenn du in einer Where-Klausel ein Feld auf explizit <> NULL abfragst. Dadurch werden die NULL-Werte wieder ausgeschlossen, die du durch LEFT ja eigentlich haben willst.

    Aber ich frage mich was die Abfrage

    WHERE Computername IS NULL

    bringen soll.
    In diesem SQL könntest du das gleichsetzen mit "where not exists ..."

    Tipp: In einem Exists-Select interessiert die Select-Liste nicht, da nur die Where-Klausel geprüft und ein BOOl-Wert geliefert wird.
    Es reicht also ein "where [not] exists (select * from mytable <Bedingung>)".

    Wenn dir mein Tipp mit den temporären Tabellen geholfen hat, würde ich mich über eine Bewertung freuen.

    Freitag, 27. November 2020 09:34
  • Ich konnte schon einen Fehler entdecken. Hab an der völlig falschen Stelle gesucht. Die @Required_SWPackages war schon nicht vollständig. Dann kann natürlich die Endausgabe auch nicht passen.

    Das hab ich jetzt gefixt. 

    Anhand des 

    WHERE Computername IS NULL

    stelle ich fest, wo ich auf der Rechten-Tabelle eben NICHTS gefunden habe. Sprich ich vergleiche ja links mein kardesisches Produkt aus kompletter Serverliste und zu suchender Software-Pakete, gegen die Liste rechts bestehend aus reell installierter Software. Diese rechte Liste hat eine Zeile pro (Server, Software, Version). Also kann jeder Server mal installierter Softwarepakete in dieser Liste vorkommen.

    Finde ich die Suchkombination aus Servername und Suchsoftware in diesem Left-Join auf der rechte Seite nicht, so ist der Computername NULL und ich weiß, dass diese Software fehlt.

    Funktionieren tut das jetzt schon. Schön ists hald noch nicht, da die Performance auf Grund der vielen notwendigen Likes leider etwas leidet. Da arbeite ich noch dran. 

    Ich honoriere euch natürlich gerne. Ist das der Abstimmungspfeil nach oben auf den Post? Sorry für die doofe Frage. War auf diesem Forum hier noch nicht so aktiv.

    Testdaten werde ich auch schauen, wie ich die geschickterweise zur Verfügung stelle. Muss die Exportscripts etwas überarbeiten. Das sprengt hier sonst den Rahmen.

    Freitag, 27. November 2020 10:15
  • Für die Likes in den übergroßen Feldern gibts noch die Volltextsuche.
    Dies ist ein Zusatzmodul, dass du aber bereits haben könntest. Hier sind dann erweiterte SQL-Befehle möglich.

    https://docs.microsoft.com/de-de/sql/relational-databases/search/full-text-search?view=sql-server-ver15

    Ist ein Volltextindex mal erstellt, gibts die Funktion "CONTAINS" statt LIKE.
    https://docs.microsoft.com/de-de/sql/t-sql/queries/contains-transact-sql?view=sql-server-ver15

    Bei den temporären Tabellen ist noch zu überlegen, SQL mit einem Index zu unterstützen:
    https://www.sqlshack.com/indexing-sql-server-temporary-tables/

    Zu bedenken ist auch, dass du ggf. per Select auf die Temptables prüfen kannst, ob die Daten u.U. schon mal ermittelt wurden. Somit kannst du dann bzgl. Paging optimieren.
    Beim nächsten Connect mit der DB sind die Tabellen erst mal leer.

    Für die Bewertungen gibts die Abstimmung (5-Punkte) oder "Antwort vorschlagen" und/oder bestätigen (20 Punkte), nicht dass ich punktegeil wäre;-).

    Freitag, 27. November 2020 10:50
  • So, ich habs jetzt abermals optimieren können. Die "teuren" Likes konnte ich jetzt eliminieren und gegen = tauschen. Alle großen Joins Fields sind nun indiziert. Konnte so die effektive Web-Anzeigezeit von 10-20 Sekunden auf vielleicht 200-300ms senken. Auch wenn ich auf das Endergebnis über das Webfrontend filtere geht das gleich schnell wie ohne filter.

    Ich danke euch nochmal vielmals

    ALTER FUNCTION udf_Device_Server_Win_Missing_Required_SW (@Mode NVARCHAR(32))
    RETURNS @MissingSW TABLE (
    	[Name] NVARCHAR(64)
    	,DisplayName NVARCHAR(256)
    	,[Version] NVARCHAR(64)
    	,Verw NVARCHAR(16)
    	,PatchAm NVARCHAR(64)
    	,DeviceType NVARCHAR(128)
    	,id INT
    --	,Computername NVARCHAR(64)
    --	,DisplayName2 NVARCHAR(256)
    	INDEX MyCIDX CLUSTERED (DisplayName,Name,Patcham,Verw)
    	)
    AS
    BEGIN
    	-- Find Softwarepackages
    	DECLARE @Required_SWPackages TABLE (
    		Displayname NVARCHAR(256)
    		,[Version] NVARCHAR(64)
    		,Verw NVARCHAR(16)
    		)
    
    	IF @mode = 'allversions'
    		INSERT INTO @Required_SWPackages
    		SELECT Installed.Displayname
    			,Installed.[Version]
    			,Req.Verw
    		FROM p_Device_Software_Installations Installed
    		INNER JOIN CFGRequiredSoftware Req ON (
    				Installed.Displayname LIKE Req.Displayname
    				AND Installed.[Version] LIKE (
    					CASE 
    						WHEN Req.[Version] IS NULL
    							THEN '%'
    						WHEN Req.[Version] = ''
    							THEN '%'
    						ELSE Req.[Version]
    						END
    					)
    				)
    		GROUP BY Installed.Displayname
    			,Installed.[Version]
    			,Verw
    	ELSE
    		INSERT INTO @Required_SWPackages
    		SELECT Displayname
    			,[Version]
    			,Verw
    		FROM CFGRequiredSoftware
    
    	-- Filter Active Servers
    	DECLARE @Active_Windows_Servers TABLE (
    		[Name] NVARCHAR(64)
    		,Verw NVARCHAR(16)
    		,PatchAm NVARCHAR(255)
    		,DeviceType NVARCHAR(128)
    		,id INT
    		)
    
    	INSERT INTO @Active_Windows_Servers
    	SELECT [Name]
    		,Verw
    		,PatchAm
    		,DeviceType
    		,id
    	FROM View_Device_Server_Win
    	WHERE InstallStatus = 'active'
    
    	-- Get currently installed (filtered by Required_SWPackages)
    	DECLARE @Currently_Installed TABLE (
    		Computername NVARCHAR(64)
    		,DisplayName NVARCHAR(256)
    		,[Version] NVARCHAR(64)
    		,Verw NVARCHAR(16)
    		,DisplayNameSS NVARCHAR(256)
    		,VersionSS Nvarchar(64)
    		,VerwSS NVARCHAR(16)
    		Index myIndex CLUSTERED (Computername,DisplayNameSS,VersionSS,VerwSS)
    		
    		)
    
    		INSERT INTO @Currently_Installed
    	SELECT View_Software_Installations.Computername
    		,View_Software_Installations.DisplayName
    		,View_Software_Installations.[Version]
    		,View_Software_Installations.Verw
    		,Required_SWPackages.Displayname
    		,Required_SWPackages.[Version]
    		,Required_SWPackages.Verw
    	FROM View_Software_Installations
    	Right Join @Required_SWPackages Required_SWPackages ON (
    			View_Software_Installations.DisplayName Like Required_SWPackages.DisplayName
    			AND View_Software_Installations.[Version] Like Required_SWPackages.[Version]
    			AND View_Software_Installations.Verw Like Required_SWPackages.Verw)
    
    
    	--- Creating cardesian product of active servers and required SW
    		DECLARE @Workinglist TABLE (
    		DisplayName NVARCHAR(256)
    		,[Version] NVARCHAR(64)
    		,[Name] NVARCHAR(64)
    		,Verw NVARCHAR(16)
    		,PatchAm NVARCHAR(255)
    		,DeviceType NVARCHAR(128)
    		,id INT
    		INDEX MyIndex CLUSTERED ([Name],Displayname)
    		)
    
    	INSERT INTO @Workinglist
    	SELECT Displayname
    			,[Version]
    			,MyServers.*
    	FROM @Required_SWPackages SW
    	--CROSS JOIN @Active_Windows_Servers MyServers
    	Full Outer Join @Active_Windows_Servers MyServers ON (MyServers.Verw Like SW.Verw)
    
    
    
    	-- Executing final Return Query
    	IF @mode = 'allversions'
    		INSERT INTO @MissingSW
    		SELECT workinglist.[Name]
    			,workinglist.DisplayName
    			,workinglist.[Version]
    			,workinglist.Verw
    			,workinglist.PatchAm
    			,workinglist.DeviceType
    			,workinglist.id
    		--	,Currently_Installed.Computername
    		--	,Currently_Installed.DisplayName
    		FROM @Workinglist workinglist
    		LEFT JOIN @Currently_Installed Currently_Installed ON (
    				workinglist.[Name] = Currently_Installed.Computername
    				AND workinglist.DisplayName = Currently_Installed.DisplayName
    				AND workinglist.[Version] = Currently_Installed.[Version]
    				)
    		WHERE Computername IS NULL
    		ORDER BY Name
    	ELSE
    			INSERT INTO @MissingSW
    		SELECT workinglist.[Name]
    			,workinglist.DisplayName
    			,workinglist.[Version]
    			,workinglist.Verw
    			,workinglist.PatchAm
    			,workinglist.DeviceType
    			,workinglist.id
    --			,Currently_Installed.Computername
    --			,Currently_Installed.DisplayName
    		FROM @Workinglist workinglist
    		LEFT JOIN @Currently_Installed Currently_Installed 
    		ON (
    			workinglist.[Name] = Currently_Installed.Computername
    			AND (Currently_Installed.DisplayNameSS = workinglist.DisplayName)
    				)
    		WHERE Computername IS NULL
    		ORDER BY Name
    
    	RETURN;
    END;

    Freitag, 27. November 2020 13:23
  • Also für mich sieht dies klar und übersichtlich und für jeden SQL-Spezi auch verständlich aus.
    Freut mich, dass ich dir helfen konnte.

    Da kann man mal sehen, dass man auch bei SQL selber optimieren muss. Die Datenbank kann einem da leider nicht alles abnehmen.

    Es gibt übrigens eine Datenbank, die über Zugriffswege Statistiken führt aus denen man sich direkt Indizes erstellen lassen kann. Eine Vollautomatik lohnt da allerdings nicht, da sonst zu viele Indizes erstellt werden, das ist dann bei Updates kontraproduktiv.


    Freitag, 27. November 2020 17:43