Auf der Suche nach einer satzbasierten Lösung für einen rollierenden Zeitraum von 30 Tagen

7

Ich habe eine gespeicherte Prozedur (MS SQL Server 2008 R2), die prüfen muss, ob 10 Punkte / Verkäufe innerhalb eines 30-Tage-Rolling-Fensters liegen. Bisher habe ich eine Lösung, die funktioniert, aber es ist keine "richtige" Lösung, da sie nicht satzbasiert ist. Ich benutze while-Schleifen, um zu verarbeiten, was los ist. Wenn ich mir die Punkte eines Agenten ansehe, nehme ich das Datum des ersten Punkts und sehe, ob 9 weitere innerhalb eines 30-Tage-Fensters liegen. Wenn ja, markiere ich sie als verarbeitet und verwendet und fahre mit dem nächsten fort. Ich denke, es muss eine "bessere" Lösung geben, die satzbasiert ist.

Um zu sehen, was ich habe, habe ich eine SQL-Geige eingerichtet. Es zeigt am Ende, dass wir 3 Agenten verarbeitet haben und 4 Anreize gegeben werden müssen. Gibt es eine bessere Möglichkeit, dies zu tun, ohne while-Schleifen zu verwenden, die mehr auf Sätzen basieren?

SQL Fiddle

Brandon
quelle
1
Außer der SQL Fiddle, die ich zusammengestellt habe?
Brandon
Gibt es eine Chance, auf SQL Server 2012 umzusteigen? Verbesserungen der Fensterfunktionen dort würden Ihre Lösung wahrscheinlich viel einfacher machen.
Aaron Bertrand
Unglücklicherweise nicht. :(
Brandon
3
Ohne Fenstersemantik kann Ihr nicht satzbasierter Ansatz tatsächlich der optimale sein (oder gut genug). Ich habe dieses spezielle Problem in Ihrem Szenario nicht untersucht, aber das war meine Schlussfolgerung für die Ausführung von Summen - die am besten unterstützte Option, IMHO, für <2012 war tatsächlich ein Cursor.
Aaron Bertrand
1
Ich verwende hierfür im Allgemeinen ein SQLCLR-Verfahren . Wenn ich morgen Zeit habe, kann ich eine Implementierung als Antwort beitragen, für die Nachwelt, wenn nichts anderes.
Paul White 9

Antworten:

1

Ich habe versucht, einen Ansatz zu entwickeln, der 2008 funktioniert und mehr auf Sets basiert. Folgendes habe ich mir ausgedacht.

Es war zwar komplexer als ich gehofft hatte, aber es könnte ein interessanter Ansatz für Sie sein, einen Vergleich mit Ihrem aktuellen Ansatz für größere Datenmengen anzustellen. Für das, was es wert ist, läuft dieses Skript in ungefähr 15 ms für den bereitgestellten Datensatz auf meinem Computer (gegenüber 75 ms für das ursprüngliche Skript).

Wie andere bereits erwähnt haben, gibt es wahrscheinlich andere bessere Ansätze, wenn Sie 2012+ Fensterfunktionen oder 2014 eine nativ kompilierte Prozedur verwenden konnten. Aber es kann Spaß machen, manchmal darüber nachzudenken, wie man Dinge ohne die neueren Funktionen macht!

http://sqlfiddle.com/#!3/ad2be/7

-- Assign each point a sequential rank within each agent's history
SELECT p.internalID, ROW_NUMBER() OVER (PARTITION BY internalID ORDER BY date) AS recordRank, date
INTO #orderedPoints
FROM points p
-- Sort the data for efficient lookup of potential incentives
ALTER TABLE #orderedPoints
ADD UNIQUE CLUSTERED (internalId, recordRank)

-- Identify a potential incentive for any point that has 9+ points in the following 30 days
SELECT s.internalId, s.recordRank, ROW_NUMBER() OVER (PARTITION BY s.internalId ORDER BY s.recordRank) AS potentialIncentiveRank
INTO #potentialIncentives
FROM #orderedPoints s
JOIN #orderedPoints e
    ON e.internalId = s.internalId
    AND e.recordRank = s.recordRank + 9
    AND e.date < DATEADD(dd, 30, s.date)
-- Sort the data to allow for efficient retrieval of subsequent incentives
ALTER TABLE #potentialIncentives
ADD UNIQUE CLUSTERED (internalId, recordRank)

-- A table to hold the incentives achieved
CREATE TABLE #incentives (internalId INT NOT NULL, recordRank INT NOT NULL)
-- A couple transient tables to hold the current "fringe" of incentives that were just inserted
CREATE TABLE #newlyProcessedIncentives (internalId INT NOT NULL, recordRank INT NOT NULL)
CREATE TABLE #newlyProcessedIncentives_forFromClause (internalId INT NOT NULL, recordRank INT NOT NULL)

-- Identify the first incentive for each agent
-- Note that TOP clauses and aggregate functions are not allowed in the recursive portion of a CTE
-- If that were allowed, this could serve as the anchor of a recursive CTE and the loop below would be the recursive portion
INSERT INTO #incentives (internalId, recordRank)
OUTPUT inserted.internalId, inserted.recordRank INTO #newlyProcessedIncentives (internalId, recordRank)
SELECT internalId, recordRank
FROM #potentialIncentives
WHERE potentialIncentiveRank = 1

-- Identify the next incentive for each agent, stopping when no further incentives are identified
WHILE EXISTS (SELECT TOP 1 * FROM #newlyProcessedIncentives)
BEGIN
    -- Transfer the most recently identified incentives, so that we can truncate the table to capture the next iteration of incentives
    TRUNCATE TABLE #newlyProcessedIncentives_forFromClause
    INSERT INTO #newlyProcessedIncentives_forFromClause (internalId, recordRank) SELECT internalId, recordRank FROM #newlyProcessedIncentives
    TRUNCATE TABLE #newlyProcessedIncentives

    -- Identify the next incentive for each agent
    INSERT INTO #incentives (internalId, recordRank)
    OUTPUT inserted.internalId, inserted.recordRank INTO #newlyProcessedIncentives (internalId, recordRank)
    SELECT nextIncentive.internalId, nextIncentive.recordRank
    FROM #newlyProcessedIncentives_forFromClause f
    CROSS APPLY (
        SELECT TOP 1 p.*
        FROM #potentialIncentives p
        WHERE p.internalId = f.internalId
            AND p.recordRank >= f.recordRank + 10 -- A new incentive can only start after all 10 points from the previous incentive
        ORDER BY p.recordRank
    ) nextIncentive 
END

-- Present the final results in the same format as the original implementation
SELECT a.internalId, a.points, i.incentives 
FROM (  -- Tabulate points
    SELECT internalId, MAX(recordRank) AS points
    FROM #orderedPoints
    GROUP BY internalId
) a
JOIN (  -- Tabulate incentives achieved
    SELECT internalId, COUNT(*) AS incentives
    FROM #incentives
    GROUP BY internalId
) i
    ON i.internalId = a.internalId
Geoff Patterson
quelle