Benutzer mit den meisten Antworten
Performanze von none-deterministic / zufälligen Werten auf Unique Index Spalten

Frage
-
Hallo,
ich versuche einen 8 Zeichen langen, eindeutigen, zufaääligen Code zu generieren. Im Moment mache ich dies mit einer Funktion, die den generierten Wert in eine Spalte mit einem Unique Index scheibt, und den Vorgang so lange wiederholt, bis das Insert keinen Fehler mehr liefert.
Der Hauptbestandteil der Funktion sieht wie folgt aus:
DECLARE @base varchar(32);
SET @base = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';WHILE @i < 1000000
BEGIN
INSERT INTO COUPONCODE WITH (TABLOCKX, HOLDLOCK) (COUPONCODE)
VALUES (SUBSTRING(@base,ABS(checksum(newid())%32),1)+SUBSTRING(@base,ABS(checksum(newid())%32),1)
+SUBSTRING(@base,ABS(checksum(newid())%32),1)+SUBSTRING(@base,ABS(checksum(newid())%32),1)
+SUBSTRING(@base,ABS(checksum(newid())%32),1)+SUBSTRING(@base,ABS(checksum(newid())%32),1)
+SUBSTRING(@base,ABS(checksum(newid())%32),1)+SUBSTRING(@base,ABS(checksum(newid())%32),1));
IF @@ERROR = 0
SET @i = @i + 1;
ELSE
SET @failures += 1;
ENDDie Fehleranzahl steigt erwartungsmäßig an. Das Problem besteht darin, dass ab ca 40 Milionen Einträge die Zeiten für ein Insert anfangen sehr hoch zu werden. Sind ca 20 Sekunden pro Million für die ersten 30 Millionen Inserts normal, steigen die Zeiten dann sehr schnell auf mehrere Stunden an.
Mir ist bewusst, dass der Index degeneriert / fragmentiert. Unabhängig vom Typ des Unique Index / Key / Primary Keys, dem Fillfaktor, clustered / noneclustered oder einem REBUILD des Index komme ich nicht in den Berech von 100 Millionen oder 1 Milliarde Inserts in aktzeptablen Zeiten. Dies sind aber Mengen, die innerhalb von 2- 3 Jahren vorkommen können.
Vielleicht ist auch mein Vorgehen falsch. Die Anforderung der Zufälligkeit und Eindeutigkeit sind aber gegeben.
Ich hoffe, Ihr könnt mir auf die eine oder andere Art weiterhelfen.
Zur Info: Die Fragmentierunggeschieht auch, wenn man einfach checksum(newid()) in einer int Spalte verwendet. Es ist also ein generelles Problem.
Danke euch
Chris
Antworten
-
Hallo Chris,
hierbei kann man von zwei Seiten an die Aufgabe herangehen.
Auf Seiten der Speicherung gelten die gleichen Bedingungen
wie man sie auch bei uniqueidentifier (Guid) vorfindet.
Wozu man im Web reichlich Diskussionsbeiträge findet, stellvertretend einer davon:
http://blogs.msdn.com/b/sqlserverfaq/archive/2010/05/27/guid-vs-int-debate.aspx
Dein Schlüssel ist mit 8 Bytes zwar nur halb so lang, verteilt sich aber ebenso
auf die gesamte Tabelle und die Defragmentierung wird dadurch extrem hoch.Je nachdem wofür der Coupon-Code verwendet wird, könnte man hier ggf.
dadurch Entlastung schaffen, dass man periodisch einen Vorrat von Codes erzeugt
und diese in einer separaten (relativ kleinen) Tabelle hält, und alle genutzten
in einer großen (partitionierten) Archivtabelle.
Denn der von Dir gesehene Einbruch findet zum Teil deswegen statt, wenn sich die
Tabelle nicht mehr im Speicher halten läßt (und später will man kaum den
Speicherbedarf an einem Code ausrichten).
Die zweite Seite wäre das Erzeugen der Codes als solches. Ich bin nun kein
Mathematiker/Statistiker (und habe auch keinen als Nachbarn ;-), so dass ich
keine passende Formel aus dem Hut zaubern kann. Bei Google findet man zwar
viel Zeugs (aber auch viel unausgegorenes ;-) Ein Hinweis, der zu lesen war,
bezieht sich auf das http://en.wikipedia.org/wiki/Birthday_problemMit der jetztigen Formel erzeugst Du einen 8 Byte (64 Bit) großen Code,
von dem effektiv (32er @base) nur 5 Byte (40 Bit) genutzt werden.
BTW: Du mußt bei Modulo einen draufzählen, damit Du korrekte Werte erhälst,
denn SUBSTRING ist in SQL 1-basiert.Anstatt nur viele Guid-Checksummen zu verwürfeln, könnte man eine
zeitliche Komponente einfügen. Wodurch man eine teilweise Sortierung erhält.
Die kann man wiederum dazu nutzen, die Fragmentierung kleiner zu halten
oder auch für Partitionierung-Funktionen nutzen. Zum anderen kann u. U.
die zeitliche Komponente genutzt werden, um Codes als obsolet zu erklären
(je nachdem wie sie verwendet werden).Im unten stehenden Beispiel hatte ich (gestern abend) eine Variante für die ersten Stellen probiert.
Dabei kommt es zu mehr Kollisionen, die allerdings, soweit ich in der Zeit testen konnte,
weniger exponentiell ansteigen.
Zudem hatte ich dort mit einem Integer als Primärschlüssel experimentiert, um die
Fragmentierung kleiner zu halten - wodurch der Speicherbedarf (zwei Schlüssel) mitwächst.SET NOCOUNT ON; GO DROP TABLE dbo.COUPONCODE, dbo.COUPONLOG; GO CREATE TABLE dbo.COUPONCODE ( COUPONID bigint IDENTITY(1, 1) NOT NULL, COUPONCODE char(8) NOT NULL, -- Andere Sortierung (mag anders bunter aussehen ;-) -- besser als Teil einer Sicht/Abfrage CUPONCODE_SCRAMBLE AS ( SUBSTRING(COUPONCODE, 8, 1) + SUBSTRING(COUPONCODE, 1, 1) + SUBSTRING(COUPONCODE, 6, 1) + SUBSTRING(COUPONCODE, 3, 1) + SUBSTRING(COUPONCODE, 4, 1) + SUBSTRING(COUPONCODE, 5, 1) + SUBSTRING(COUPONCODE, 2, 1) + SUBSTRING(COUPONCODE, 7, 1)), CONSTRAINT PK_COUPONCODE PRIMARY KEY /* NONCLUSTERED */ (COUPONID), CONSTRAINT UK_COUPONCODE UNIQUE (COUPONCODE) -- WITH (FILLFACTOR = 75) ); GO CREATE TABLE dbo.COUPONLOG ( id int IDENTITY(1, 1) NOT NULL PRIMARY KEY, Zeit datetime2 NOT NULL, ZeitMs int NOT NULL, Gesamt bigint NOT NULL, Anzahl int NOT NULL, Fehler int NOT NULL); GO SET NOCOUNT ON; DECLARE @base char(32) = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; DECLARE @baseTime datetime2 = CAST('20100101' AS datetime2); DECLARE @baseValue int; DECLARE @baseId int; DECLARE @i int = 0; DECLARE @inserts int = 0, @failures int = 0; DECLARE @starttime datetime2 = SYSDATETIME(); BEGIN TRAN WHILE @i < 10000000 BEGIN BEGIN TRY -- Für 10 Minuten ab 1.1.2010 reicht es etwa 10 Jahre -- SET @baseValue = DATEDIFF(mi, @baseTime, SYSDATETIME()) / 6; -- Im Test je 10 Sekunden da kurzer Zeitraum - nicht direkt vergleichbar! SET @baseValue = DATEDIFF(ss, @baseTime, SYSUTCDATETIME()) / 6; SET @baseId = ABS(CHECKSUM(NEWID())); INSERT INTO dbo.COUPONCODE WITH (TABLOCKX, HOLDLOCK) (COUPONCODE) VALUES ( -- alternativer Divisor möglich (je nach Zeitraum) + SUBSTRING(@base, ((@baseValue / 32 / 32 / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseValue / 32 / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseValue / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseValue) % 32) + 1, 1) + SUBSTRING(@base, ((@baseId / 32 / 32 / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseId / 32 / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseId / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseId) % 32) + 1, 1)); SET @i += 1; SET @inserts += 1; IF (@i % 50000) = 0 BEGIN -- PRINT CAST(@i AS nvarchar(10)) + N' Inserts, ' + CAST(@failures AS nvarchar(10)) + N' Failures'; INSERT INTO dbo.COUPONLOG(Zeit, ZeitMs, Gesamt, Anzahl, Fehler) VALUES( SYSDATETIME(), DATEDIFF(ms, @starttime, SYSDATETIME()), (SELECT COUNT_BIG(*) FROM dbo.COUPONCODE), @inserts, @failures); SET @inserts = 0; SET @failures = 0; COMMIT; BEGIN TRAN; END; END TRY BEGIN CATCH SET @failures += 1; END CATCH; END; COMMIT; GO SELECT * , CAST(ZeitMs AS float) / CAST(NULLIF(Gesamt,0) AS float) AS GesamtMs , CAST(ZeitMs AS float) / CAST(NULLIF(Anzahl,0) AS float) AS AnzahlMs , CAST(Fehler * 100.0 AS float) / CAST(NULLIF(Anzahl,0) AS float) AS FehlerPrz FROM COUPONLOG ORDER BY id desc; GO DBCC SHOWCONTIG ('dbo.COUPONCODE') WITH TABLERESULTS, ALL_INDEXES; GO --ALTER INDEX ALL ON dbo.COUPONCODE REBUILD; --WITH (FILLFACTOR=100); GO --SELECT TOP(10000) * FROM dbo.COUPONCODE WITH (NOLOCK) ORDER BY COUPONCODE DESC; GO
Im Nachhinein betrachtet (nicht mehr probiert), wäre eine Alternative, einen Big Integer
oder zwei Integer zu nutzen, wo man die Stellen zur Laufzeit - wie beim COUPONCODE_SCRAMBLE
in einen Code umwandelt. Mit Bit-Schuppserei sollte das über einen Identitätswert +
Zeit-Komponente in einen (pseudozufälligen) Code zu transponieren sein.Wie man sieht habe ich auch mit dem FillFactor rumgespielt. Nur auf einer sehr großen Tabelle
ist der eher kontraproduktiv, so dass eine Teilung in einen aktiven und Archivteil dabei ratsamer ist.Gruß Elmar
- Als Antwort vorgeschlagen Robert BreitenhoferModerator Mittwoch, 23. Juni 2010 08:51
- Als Antwort markiert Andrei Talmaciu Freitag, 25. Juni 2010 09:46
Alle Antworten
-
Hallo Chris,
hierbei kann man von zwei Seiten an die Aufgabe herangehen.
Auf Seiten der Speicherung gelten die gleichen Bedingungen
wie man sie auch bei uniqueidentifier (Guid) vorfindet.
Wozu man im Web reichlich Diskussionsbeiträge findet, stellvertretend einer davon:
http://blogs.msdn.com/b/sqlserverfaq/archive/2010/05/27/guid-vs-int-debate.aspx
Dein Schlüssel ist mit 8 Bytes zwar nur halb so lang, verteilt sich aber ebenso
auf die gesamte Tabelle und die Defragmentierung wird dadurch extrem hoch.Je nachdem wofür der Coupon-Code verwendet wird, könnte man hier ggf.
dadurch Entlastung schaffen, dass man periodisch einen Vorrat von Codes erzeugt
und diese in einer separaten (relativ kleinen) Tabelle hält, und alle genutzten
in einer großen (partitionierten) Archivtabelle.
Denn der von Dir gesehene Einbruch findet zum Teil deswegen statt, wenn sich die
Tabelle nicht mehr im Speicher halten läßt (und später will man kaum den
Speicherbedarf an einem Code ausrichten).
Die zweite Seite wäre das Erzeugen der Codes als solches. Ich bin nun kein
Mathematiker/Statistiker (und habe auch keinen als Nachbarn ;-), so dass ich
keine passende Formel aus dem Hut zaubern kann. Bei Google findet man zwar
viel Zeugs (aber auch viel unausgegorenes ;-) Ein Hinweis, der zu lesen war,
bezieht sich auf das http://en.wikipedia.org/wiki/Birthday_problemMit der jetztigen Formel erzeugst Du einen 8 Byte (64 Bit) großen Code,
von dem effektiv (32er @base) nur 5 Byte (40 Bit) genutzt werden.
BTW: Du mußt bei Modulo einen draufzählen, damit Du korrekte Werte erhälst,
denn SUBSTRING ist in SQL 1-basiert.Anstatt nur viele Guid-Checksummen zu verwürfeln, könnte man eine
zeitliche Komponente einfügen. Wodurch man eine teilweise Sortierung erhält.
Die kann man wiederum dazu nutzen, die Fragmentierung kleiner zu halten
oder auch für Partitionierung-Funktionen nutzen. Zum anderen kann u. U.
die zeitliche Komponente genutzt werden, um Codes als obsolet zu erklären
(je nachdem wie sie verwendet werden).Im unten stehenden Beispiel hatte ich (gestern abend) eine Variante für die ersten Stellen probiert.
Dabei kommt es zu mehr Kollisionen, die allerdings, soweit ich in der Zeit testen konnte,
weniger exponentiell ansteigen.
Zudem hatte ich dort mit einem Integer als Primärschlüssel experimentiert, um die
Fragmentierung kleiner zu halten - wodurch der Speicherbedarf (zwei Schlüssel) mitwächst.SET NOCOUNT ON; GO DROP TABLE dbo.COUPONCODE, dbo.COUPONLOG; GO CREATE TABLE dbo.COUPONCODE ( COUPONID bigint IDENTITY(1, 1) NOT NULL, COUPONCODE char(8) NOT NULL, -- Andere Sortierung (mag anders bunter aussehen ;-) -- besser als Teil einer Sicht/Abfrage CUPONCODE_SCRAMBLE AS ( SUBSTRING(COUPONCODE, 8, 1) + SUBSTRING(COUPONCODE, 1, 1) + SUBSTRING(COUPONCODE, 6, 1) + SUBSTRING(COUPONCODE, 3, 1) + SUBSTRING(COUPONCODE, 4, 1) + SUBSTRING(COUPONCODE, 5, 1) + SUBSTRING(COUPONCODE, 2, 1) + SUBSTRING(COUPONCODE, 7, 1)), CONSTRAINT PK_COUPONCODE PRIMARY KEY /* NONCLUSTERED */ (COUPONID), CONSTRAINT UK_COUPONCODE UNIQUE (COUPONCODE) -- WITH (FILLFACTOR = 75) ); GO CREATE TABLE dbo.COUPONLOG ( id int IDENTITY(1, 1) NOT NULL PRIMARY KEY, Zeit datetime2 NOT NULL, ZeitMs int NOT NULL, Gesamt bigint NOT NULL, Anzahl int NOT NULL, Fehler int NOT NULL); GO SET NOCOUNT ON; DECLARE @base char(32) = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; DECLARE @baseTime datetime2 = CAST('20100101' AS datetime2); DECLARE @baseValue int; DECLARE @baseId int; DECLARE @i int = 0; DECLARE @inserts int = 0, @failures int = 0; DECLARE @starttime datetime2 = SYSDATETIME(); BEGIN TRAN WHILE @i < 10000000 BEGIN BEGIN TRY -- Für 10 Minuten ab 1.1.2010 reicht es etwa 10 Jahre -- SET @baseValue = DATEDIFF(mi, @baseTime, SYSDATETIME()) / 6; -- Im Test je 10 Sekunden da kurzer Zeitraum - nicht direkt vergleichbar! SET @baseValue = DATEDIFF(ss, @baseTime, SYSUTCDATETIME()) / 6; SET @baseId = ABS(CHECKSUM(NEWID())); INSERT INTO dbo.COUPONCODE WITH (TABLOCKX, HOLDLOCK) (COUPONCODE) VALUES ( -- alternativer Divisor möglich (je nach Zeitraum) + SUBSTRING(@base, ((@baseValue / 32 / 32 / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseValue / 32 / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseValue / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseValue) % 32) + 1, 1) + SUBSTRING(@base, ((@baseId / 32 / 32 / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseId / 32 / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseId / 32) % 32) + 1, 1) + SUBSTRING(@base, ((@baseId) % 32) + 1, 1)); SET @i += 1; SET @inserts += 1; IF (@i % 50000) = 0 BEGIN -- PRINT CAST(@i AS nvarchar(10)) + N' Inserts, ' + CAST(@failures AS nvarchar(10)) + N' Failures'; INSERT INTO dbo.COUPONLOG(Zeit, ZeitMs, Gesamt, Anzahl, Fehler) VALUES( SYSDATETIME(), DATEDIFF(ms, @starttime, SYSDATETIME()), (SELECT COUNT_BIG(*) FROM dbo.COUPONCODE), @inserts, @failures); SET @inserts = 0; SET @failures = 0; COMMIT; BEGIN TRAN; END; END TRY BEGIN CATCH SET @failures += 1; END CATCH; END; COMMIT; GO SELECT * , CAST(ZeitMs AS float) / CAST(NULLIF(Gesamt,0) AS float) AS GesamtMs , CAST(ZeitMs AS float) / CAST(NULLIF(Anzahl,0) AS float) AS AnzahlMs , CAST(Fehler * 100.0 AS float) / CAST(NULLIF(Anzahl,0) AS float) AS FehlerPrz FROM COUPONLOG ORDER BY id desc; GO DBCC SHOWCONTIG ('dbo.COUPONCODE') WITH TABLERESULTS, ALL_INDEXES; GO --ALTER INDEX ALL ON dbo.COUPONCODE REBUILD; --WITH (FILLFACTOR=100); GO --SELECT TOP(10000) * FROM dbo.COUPONCODE WITH (NOLOCK) ORDER BY COUPONCODE DESC; GO
Im Nachhinein betrachtet (nicht mehr probiert), wäre eine Alternative, einen Big Integer
oder zwei Integer zu nutzen, wo man die Stellen zur Laufzeit - wie beim COUPONCODE_SCRAMBLE
in einen Code umwandelt. Mit Bit-Schuppserei sollte das über einen Identitätswert +
Zeit-Komponente in einen (pseudozufälligen) Code zu transponieren sein.Wie man sieht habe ich auch mit dem FillFactor rumgespielt. Nur auf einer sehr großen Tabelle
ist der eher kontraproduktiv, so dass eine Teilung in einen aktiven und Archivteil dabei ratsamer ist.Gruß Elmar
- Als Antwort vorgeschlagen Robert BreitenhoferModerator Mittwoch, 23. Juni 2010 08:51
- Als Antwort markiert Andrei Talmaciu Freitag, 25. Juni 2010 09:46
-
Hallo Elmar,
dank dir erst einmal für deine Mühe. Ich werde es geleich einmal mit deinem Beispiel bei uns testen und experimentieren.
Ich habe aber noch nicht den Sinn der COUPONCODE_SCRAMBLE Spalte verstanden, da dort ja nur der Code anders aufgeschlüsselt wird. Hilf mir dort bitte auf die Sprünge.
Danke
Gruß Chris
-
Hallo Chris,
wie Du selbst anmerkst:
Durch die Einführung von bedingt deterministischen Werten am Anfang der Zeichenkette,
sind die Werte leichter zu "erraten".
Ob das in Deinem Falle überhaupt eine Rolle spielt, kann ich nicht beurteilen.Ich hatte dem nicht viel Aufmerksamkeit geschenkt und es nur als Option stehen lassen.
Auch im Hinblick auf die angesprochene Alternative, Integer für Zeitanteile + Identitätswert
zu verwenden, womit die Werte noch offensichtlicher erkennbar würden.
Und man einiges an Bitschubserei verwenden müsste, um es nicht zu offensichlich werden
zu lassen - der SQL Server kennt dankenswerter Weise zumindest einige Bitoperatoren.Gruß Elmar