Il y a quelques défis avec cette question. Les index dans SQL Server peuvent effectuer les opérations suivantes de manière très efficace avec seulement quelques lectures logiques chacun:
- vérifier qu'une ligne existe
- vérifier qu'il n'y a pas de ligne
- trouver la ligne suivante commençant à un moment donné
- trouver la ligne précédente commençant à un moment donné
Cependant, ils ne peuvent pas être utilisés pour rechercher la Nème ligne dans un index. Pour ce faire, vous devez rouler votre propre index stocké sous forme de table ou analyser les N premières lignes de l'index. Votre code C # repose fortement sur le fait que vous pouvez trouver efficacement le Nème élément du tableau, mais vous ne pouvez pas le faire ici. Je pense que cet algorithme n'est pas utilisable pour T-SQL sans changement de modèle de données.
Le deuxième défi concerne les restrictions sur les BINARY
types de données. Autant que je sache, vous ne pouvez pas effectuer d'addition, de soustraction ou de division de la manière habituelle. Vous pouvez convertir votre BINARY(64)
en un BIGINT
et il ne générera pas d'erreurs de conversion, mais le comportement n'est pas défini :
Les conversions entre n'importe quel type de données et les types de données binaires ne sont pas garanties d'être identiques entre les versions de SQL Server.
De plus, l'absence d'erreurs de conversion est en quelque sorte un problème ici. Vous pouvez convertir quelque chose de plus grand que la plus grande BIGINT
valeur possible , mais cela vous donnera les mauvais résultats.
Il est vrai que vous avez actuellement des valeurs supérieures à 9223372036854775807. Cependant, si vous commencez toujours à 1 et recherchez la plus petite valeur minimale, ces grandes valeurs ne peuvent être pertinentes que si votre table contient plus de 9223372036854775807 lignes. Cela semble peu probable car votre table à ce moment-là serait d'environ 2000 exaoctets, donc pour répondre à votre question, je vais supposer que les très grandes valeurs n'ont pas besoin d'être recherchées. Je vais également faire une conversion de type de données car elles semblent inévitables.
Pour les données de test, j'ai inséré l'équivalent de 50 millions d'entiers séquentiels dans un tableau avec 50 millions d'entiers supplémentaires avec un écart de valeur unique toutes les 20 valeurs. J'ai également inséré une valeur unique qui ne rentre pas correctement dans une signature BIGINT
:
CREATE TABLE dbo.BINARY_PROBLEMS (
KeyCol BINARY(64) NOT NULL
);
INSERT INTO dbo.BINARY_PROBLEMS WITH (TABLOCK)
SELECT CAST(SUM(OFFSET) OVER (ORDER BY (SELECT NULL) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BINARY(64))
FROM
(
SELECT 1 + CASE WHEN t.RN > 50000000 THEN
CASE WHEN ABS(CHECKSUM(NewId()) % 20) = 10 THEN 1 ELSE 0 END
ELSE 0 END OFFSET
FROM
(
SELECT TOP (100000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
) t
) tt
OPTION (MAXDOP 1);
CREATE UNIQUE CLUSTERED INDEX CI_BINARY_PROBLEMS ON dbo.BINARY_PROBLEMS (KeyCol);
-- add a value too large for BIGINT
INSERT INTO dbo.BINARY_PROBLEMS
SELECT CAST(0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000 AS BINARY(64));
Ce code a mis quelques minutes à s'exécuter sur ma machine. J'ai fait en sorte que la première moitié du tableau ne présente aucun écart pour représenter une sorte de pire cas de performance. Le code que j'ai utilisé pour résoudre le problème scanne l'index dans l'ordre afin qu'il se termine très rapidement si le premier écart est au début du tableau. Avant d'en arriver là, vérifions que les données sont telles qu'elles devraient être:
SELECT TOP (2) KeyColBigInt
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
FROM dbo.BINARY_PROBLEMS
) t
ORDER By KeyCol DESC;
Les résultats suggèrent que la valeur maximale à laquelle nous convertissons BIGINT
est 102500672:
╔══════════════════════╗
║ KeyColBigInt ║
╠══════════════════════╣
║ -9223372036854775808 ║
║ 102500672 ║
╚══════════════════════╝
Il y a 100 millions de lignes avec des valeurs qui correspondent à BIGINT comme prévu:
SELECT COUNT(*)
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF;
Une approche à ce problème consiste à analyser l'index dans l'ordre et à quitter dès que la valeur d'une ligne ne correspond pas à la ROW_NUMBER()
valeur attendue . La table entière n'a pas besoin d'être scannée pour obtenir la première ligne: uniquement les lignes jusqu'au premier écart. Voici une façon d'écrire du code susceptible d'obtenir ce plan de requête:
SELECT TOP (1) KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF
) t
WHERE KeyColBigInt <> RN
ORDER BY KeyCol;
Pour des raisons qui ne rentrent pas dans cette réponse, cette requête sera souvent exécutée en série par SQL Server et SQL Server sous-estimera souvent le nombre de lignes à analyser avant de trouver la première correspondance. Sur ma machine, SQL Server analyse 50000022 lignes à partir de l'index avant de trouver la première correspondance. L'exécution de la requête prend 11 secondes. Notez que cela renvoie la première valeur après l'écart. On ne sait pas exactement quelle ligne vous voulez exactement, mais vous devriez pouvoir modifier la requête en fonction de vos besoins sans trop de problèmes. Voici à quoi ressemble le plan :
Ma seule autre idée était d'intimider SQL Server pour qu'il utilise le parallélisme pour la requête. J'ai quatre processeurs, donc je vais diviser les données en quatre plages et faire des recherches sur ces plages. Chaque CPU se verra attribuer une plage. Pour calculer les plages, j'ai simplement saisi la valeur maximale et j'ai supposé que les données étaient uniformément réparties. Si vous voulez être plus intelligent à ce sujet, vous pouvez consulter un histogramme de statistiques échantillonné pour les valeurs de colonne et créer vos plages de cette façon. Le code ci-dessous repose sur de nombreuses astuces non documentées qui ne sont pas sûres pour la production, y compris l' indicateur de trace 8649 :
SELECT TOP 1 ca.KeyCol
FROM (
SELECT 1 bucket_min_value, 25625168 bucket_max_value
UNION ALL
SELECT 25625169, 51250336
UNION ALL
SELECT 51250337, 76875504
UNION ALL
SELECT 76875505, 102500672
) buckets
CROSS APPLY (
SELECT TOP 1 t.KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, buckets.bucket_min_value - 1 + ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol >= CAST(buckets.bucket_min_value AS BINARY(64)) AND KeyCol <= CAST(buckets.bucket_max_value AS BINARY(64))
) t
WHERE t.KeyColBigInt <> t.RN
ORDER BY t.KeyCol
) ca
ORDER BY ca.KeyCol
OPTION (QUERYTRACEON 8649);
Voici à quoi ressemble le modèle de boucle imbriquée parallèle:
Dans l'ensemble, la requête fait plus de travail qu'auparavant, car elle analysera plus de lignes dans le tableau. Cependant, il fonctionne maintenant en 7 secondes sur mon bureau. Il pourrait mieux se paralléliser sur un vrai serveur. Voici un lien vers le plan réel .
Je ne peux vraiment pas penser à un bon moyen de résoudre ce problème. Faire le calcul en dehors de SQL ou modifier le modèle de données peut être votre meilleur choix.
delete
déclencheur sur la table qui viderait le binaire maintenant disponible dans une table séparée (par exemple,create table available_for_reuse(id binary64)
), surtout à la lumière de la nécessité de faire cette recherche très fréquemment ?