petere
Goto Top

Abfrage nach LIFO-Prinzip

Hallo,
ich scheitere an einer performanten Abfrage in MSSQL, in welcher ich für einen Artikel unter Abfrage der "Einkaufstabelle" ermitteln kann, was der aktuelle Warenbestand in EUR wert ist. Beispiel:

Tabelle: Artikel
Artikelnummer | Bestand
abc | 100
bbb | 50
...

Tabelle: Einkauf
Artikelnummer | MengeGekauft | PreisGekauft | Datum
abc | 50 Stück | 1 € | 16.11.2016
abc | 40 Stück | 1,50 € | 12.11.2016
abc | 20 Stück | 2,20 € | 10.11.2016
abc | 50 Stück | 1 € | 16.10.2016
...

Rechenweg:
"Rechne solange rekursiv, bis der Bestand über die MengeGekauft gedeckt ist"
also:
50 Stück zu 1 € = 50 €
[rest 50 Stück]
40 Stück zu 1,50 € = 60 €
[rest 10 Stück]
10 Stück zu 2,20 € = 22 €

Summe 50€ + 60€ + 22€ = 132€
Aufteilung auf 100 Stück: 132 € / 100 Stück = 1,32 € / Stück

Danke für eure Tipps face-smile
Viele Grüße,
Peter

Content-ID: 321171

Url: https://administrator.de/contentid/321171

Ausgedruckt am: 22.11.2024 um 20:11 Uhr

Kraemer
Kraemer 16.11.2016 um 17:00:24 Uhr
Goto Top
Moin,
Zitat von @petere:
ich scheitere an einer performanten Abfrage in MSSQL,
wie sieht denn deine nicht performante Lösung aus?

Gruß Krämer
petere
petere 16.11.2016 um 20:26:38 Uhr
Goto Top
Ich laufe alle Artikel in .NET in einer Schleife durch, ermittle den Bestand und rechne die Werte aus der Tabelle Einkauf rückwärts, bis ich bei 0 bin. Meine Lösung ist quasi fast ohne SQL, und dauert bei 2 Mio Datensätzen ewig.
ukulele-7
ukulele-7 17.11.2016 um 08:53:50 Uhr
Goto Top
Eigentlich kann das mit den beiden Tabellen nicht korrekt funktionieren.

Angenommen du berechnest jetzt deinen Stückpreis von 1,32 Euro. Jetzt kaufst du 100 neue Einheiten zu je 0,80 Euro, dann ändert sich nachträglich wieder dein Ergebnis? Eigentlich musst du doch reale Abgänge in einer Tabelle festhalten. - Oder bin ich jetzt verwirrt?
petere
petere 17.11.2016 um 08:58:36 Uhr
Goto Top
Nein, die Berechnung ist nicht tagesaktuell fließend sondern zu einem Stichtag (z.B. HEUTE). Dann wird der Wert am Stichtag ausgedruckt und archiviert.
ukulele-7
Lösung ukulele-7 17.11.2016 um 12:08:54 Uhr
Goto Top
WITH Artikel(Artikelnummer,Bestand) AS (
	SELECT	'abc',100 UNION ALL  
	SELECT	'bbb',50  
	), Einkauf(Artikelnummer,MengeGekauft,PreisGekauft,Datum) AS (
	SELECT	'abc',50,1,cast('16.11.2016' AS DATE) UNION ALL  
	SELECT	'abc',40,1.5,'12.11.2016' UNION ALL  
	SELECT	'abc',20,2.2,'10.11.2016' UNION ALL  
	SELECT	'abc',50,1,'16.10.2016'  
	), t1 AS (
	SELECT	e.Artikelnummer,
			e.MengeGekauft,
			e.PreisGekauft,
			e.Datum,
			(	SELECT	sum(MengeGekauft)
				FROM	Einkauf
				WHERE	Artikelnummer = e.Artikelnummer
				AND		Datum >= e.Datum ) AS LIFO
	FROM	Einkauf e
	), t2 AS (
	SELECT	t1.Artikelnummer,
			t1.PreisGekauft,
			(	CASE
				WHEN	t1.LIFO - t1.MengeGekauft < a.Bestand
				AND		t1.LIFO > a.Bestand
				THEN	t1.LIFO - a.Bestand
				WHEN	t1.LIFO < a.Bestand
				THEN	t1.MengeGekauft
				END ) AS Relevant
	FROM	t1
	LEFT JOIN Artikel a
	ON		t1.Artikelnummer = a.Artikelnummer
	WHERE	t1.LIFO - t1.MengeGekauft < a.Bestand
	)
SELECT	t2.Artikelnummer,
		sum(t2.Relevant) AS Stück,
		sum(t2.Relevant * t2.PreisGekauft) AS Preis,
		sum(t2.Relevant * t2.PreisGekauft) / sum(t2.Relevant) AS Durchschnittspreis
FROM	t2
GROUP BY t2.Artikelnummer
Die ersten beiden Tabellen im WITH-Teil bilden nur deine Testdaten nach. t1, t2 und der SELECT nach dem WITH-Teil sind im Prinzip drei verschachtelte Abfragen. Ich bin mir nicht sicher ob man das nicht noch etwas verschlanken kann und vor allem wirst du vielleicht noch auf Performance optimieren müssen (mit Indexen z.B.).
petere
petere 20.11.2016 um 18:15:39 Uhr
Goto Top
Vielen Dank, ich hab die Funktion selbst getestet & das korrekte Ergebnis erhalten.
Dann habe ich Artikel und Einkauf an die echten Daten in MSSQL gebunden und getestet
- SELECT * FROM Artikel --> okay, liefert richtige Werte
- SELECT * FROM Einkauf--> okay, liefert richtige Werte
- SELECT * FROM t1--> okay, liefert richtige Werte

Nur bei t2 liefert er keine Ergebnismenge.

Ich habe alle Zahlenwerte einheitlich auf real gecastet, die Artikelnummer ist einheitlich ein nvarchar(20).
Deine CAST as Date wird leider nicht akzeptiert ...

Fehler: Der DATE-Typ ist kein definierter Systemtyp.


Möglicherweise hängt es daran?
Biber
Biber 21.11.2016 aktualisiert um 09:40:33 Uhr
Goto Top
Moin petere,

was immer du getestet hast: ein "CAST(.. as Date)" kann da nicht enthalten sein.
Wenn du deine "echten" Tabellen einsetzt, dann geht doch alles quasi erst mit ukelele-7s Zeile 09 los

 with t1 AS (
	SELECT	e.Artikelnummer,
			e.MengeGekauft,
			e.PreisGekauft,
			e.Datum,
			(	SELECT	sum(MengeGekauft)
				FROM	Einkauf
				WHERE	Artikelnummer = e.Artikelnummer
				AND		Datum >= e.Datum ) AS LIFO
	FROM	Einkauf e
                                -- zum Testen unbedingt einschränken!!
                                -- WHERE e.Artikelnummer ='Art4711' -- irgendein real existierender Artikel natürlich   

	), t2 AS (
	SELECT	t1.Artikelnummer,
			t1.PreisGekauft,
			(	CASE
				WHEN	t1.LIFO - t1.MengeGekauft < a.Bestand
				AND		t1.LIFO > a.Bestand
				THEN	t1.LIFO - a.Bestand
				WHEN	t1.LIFO < a.Bestand
				THEN	t1.MengeGekauft
				END ) AS Relevant
	FROM	t1
	LEFT JOIN Artikel a
	ON		t1.Artikelnummer = a.Artikelnummer
	WHERE	t1.LIFO - t1.MengeGekauft < a.Bestand
	)
SELECT	t2.Artikelnummer,
		sum(t2.Relevant) AS Stück,
		sum(t2.Relevant * t2.PreisGekauft) AS Preis,
		sum(t2.Relevant * t2.PreisGekauft) / sum(t2.Relevant) AS Durchschnittspreis
FROM	t2
GROUP BY t2.Artikelnummer

Da is' nix zu CASTEN.
Jedenfalls nicht, wenn "Artikelnummer" in den Tabellen "Einkauf" und "Bestand" denselben Datentyp hat.
Sollte dein "Datum"sfeld in Tabelle "Einkauf" gar kein Datumsfeld sein, sondern ein nvarchar() oder ähnliches, dann brauchen wir nicht weiterzumachen - wenn du in einer Query 2 Mio Datensätze* 2 Felder von nvarchar() auf datetime casten willst... das hat keine Zukunft.

Wenn dein MSSQL keinen Typ DATE kennt, dann deutet das aber auf ein recht altes MSSQL hin... so 2005 oder so?
Damals musstest du ggf. auf DATETIME casten, weil DATE noch nicht erfunden war.

Anmerkung zur Robustheit:
Ukulele-7s Lösung oben geht genau in die richtige Richtung.
Aber in einigen Situationen geht es "natürlich" schief, weil von idealen Daten ausgegangen wird:

Wenn die Beispieldaten nicht so sind:
abc | 50 Stück | 1 € | 16.11.2016
abc | 40 Stück | 1,50 € | 12.11.2016
abc | 20 Stück | 2,20 € | 10.11.2016
abc | 50 Stück | 1 € | 16.10.2016

Sondern so:

abc | 50 Stück | 1 € | 16.11.2016
abc | 40 Stück | 1,50 € | 12.11.2016
abc | 10 Stück | 2,20 € | 10.11.2016 Zwei Artikeleinkäufe an einem Tag (gleicher Preis)
abc | 10 Stück | 2,20 € | 10.11.2016
abc | 50 Stück | 1 € | 16.10.2016

Oder so:

abc | 50 Stück | 1 € | 16.11.2016
abc | 40 Stück | 1,50 € | 12.11.2016
abc | 20 Stück | 2,20 € | 10.11.2016 Zwei Artikeleinkäufe an einem Tag (unterschiedlicher Preis)
abc | 10 Stück | 2,50 € | 10.11.2016
abc | 50 Stück | 1 € | 16.10.2016

.. da sind die Ergebnisse nicht ganz so verlässlich.
Soll nur heissen: ein bisschen Testen der Extremfälle, die es im realen Leben immer geben wird, sollte noch gemacht werden.

Grüße
Biber
ukulele-7
ukulele-7 21.11.2016 um 11:22:14 Uhr
Goto Top
Richtig, cast() sollte eigentlich nicht zum Einsatz kommen. Entscheidend ist nichtmal das es DATE oder DATETIME ist sondern nur das die Sortierung stimmt, also ginge theoretisch auch irgendein INT zu diesem Zweck.
petere
petere 24.11.2016 um 07:53:30 Uhr
Goto Top
Ich danke euch beiden vielmals, ich habe nun meine Daten vorab bereinigt über ein vorgelagertes SELECT, dass die Qualität stimmt. Jetzt passen die Ergebnisse und die Performance ist top (Indices waren vorhanden).

VIELEN DANK!
Peter