J'ai besoin de pouvoir localiser un élément manquant dans une table avec des dizaines de millions de lignes, et possède une clé primaire d'une BINARY(64)
colonne (qui est la valeur d'entrée à partir de laquelle calculer). Ces valeurs sont principalement insérées dans l'ordre, mais à l'occasion, je souhaite réutiliser une valeur précédente qui a été supprimée. Il est impossible de modifier les enregistrements supprimés avec une IsDeleted
colonne, car parfois une ligne est insérée qui est plusieurs millions de valeurs en avance sur les lignes existantes. Cela signifie que les données d'exemple devraient ressembler à ceci:
KeyCol : BINARY(64)
0x..000000000001
0x..000000000002
0x..FFFFFFFFFFFF
Ainsi, l'insertion de toutes les valeurs manquantes entre 0x000000000002
et 0xFFFFFFFFFFFF
est impossible, la quantité de temps et d'espace utilisé serait indésirable. Essentiellement, lorsque j'exécute l'algorithme, je m'attends à ce qu'il revienne 0x000000000003
, ce qui est la première ouverture.
J'ai trouvé un algorithme de recherche binaire en C #, qui interrogerait la base de données pour chaque valeur à la position i
et tester si cette valeur était attendue. Pour le contexte, mon terrible algorithme: /codereview/174498/binary-search-for-a-missing-or-default-value-by-a-given-formula
Cet algorithme exécuterait, par exemple, 26-27 requêtes SQL sur une table avec 100 000 000 éléments. (Cela ne semble pas beaucoup, mais cela se produira très fréquemment.) Actuellement, ce tableau contient environ 50 000 000 de lignes et les performances deviennent perceptibles .
Ma première pensée alternative est de traduire cela en une procédure stockée, mais qui a ses propres obstacles. (Je dois écrire un BINARY(64) + BINARY(64)
algorithme, ainsi qu'un tas d'autres choses.) Ce serait douloureux, mais pas irréalisable. J'ai également envisagé de mettre en œuvre l' algorithme de traduction basé sur ROW_NUMBER
, mais j'ai un très mauvais pressentiment à ce sujet. (A BIGINT
n'est pas assez grand pour ces valeurs.)
Je suis prêt pour d' autres suggestions, car j'ai vraiment besoin que cela soit aussi rapide que possible. Pour ce que ça vaut la seule colonne sélectionnée par la requête C # est la KeyCol
, les autres ne sont pas pertinentes pour cette partie.
En outre, pour ce que cela vaut, la requête actuelle qui récupère l'enregistrement approprié va dans le sens de:
SELECT [KeyCol]
FROM [Table]
ORDER BY [KeyCol] ASC
OFFSET <VALUE> ROWS FETCH FIRST 1 ROWS ONLY
Où <VALUE>
est l'index fourni par l'algorithme. Je n'ai pas encore eu de BIGINT
problème avec OFFSET
, mais je le ferai. (Avoir seulement 50 000 000 lignes en ce moment signifie qu'il ne demande jamais d'index au-dessus de cette valeur, mais à un moment donné, il dépassera la BIGINT
plage.)
Quelques données supplémentaires:
- Des suppressions, le
gap:sequential
rapport est d'environ1:20
; - Les 35 000 dernières lignes du tableau ont des valeurs>
BIGINT
maximum;
la source
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 ?mynameisebrown
ce qui signifierait que vous obtiendriezmynameisebrowo
, que vous ne voudrait pas siabc
est disponible.select t1.keycol+1 as aa from t as t1 where not exists (select 1 from t as t2 where t2.keycol = t1.keycol+1) order by keycol fetch first 1 rows only
vous apporte une requête ?SELECT TOP 1 ([T1].[KeyCol] + 1) AS [AA] FROM [SearchTestTableProper] AS [T1] WHERE NOT EXISTS (SELECT 1 FROM [SearchTestTableProper] AS [T2] WHERE [T2].[KeyCol] = [T1].[KeyCol] + 1) ORDER BY [KeyCol]
, qui revient toujours1
.Réponses:
Joe a déjà touché la plupart des points que je viens de passer une heure à taper, en résumé:
KeyCol
valeurs <bigint
max (9.2e18), donc les conversions (si nécessaire) vers / depuisbigint
ne devraient pas être un problème tant que vous limitez les recherches àKeyCol <= 0x00..007FFFFFFFFFFFFFFF
Alors que faire?
Mettons en suspens pendant une minute l'idée de recherche (répétée, intensive en CPU, force brute) et regardons la situation dans son ensemble.
J'aimerais proposer quelques ajouts au modèle de données ...
KeyCol
valeurs «disponibles à utiliser» , par exemple:available_for_use(KeyCol binary(64) not null primary key)
KeyCol
valeurs (peut-être créer un proc stocké 'top off'?) [par exemple, mettre à jour laselect/top/row_number()
requête de Joe pour faire untop 100000
]available_for_use
au cas où vous commenceriez à manquer de valeursKeyCol
valeurs supprimées dans notre nouvelle tableavailable_for_use
chaque fois qu'une ligne est supprimée de la table principaleKeyCol
colonne, alors un déclencheur UPDATE nouveau / modifié sur la table principale pour garder également notre nouvelle table àavailable_for_use
jourKeyCol
valeur, vousselect min(KeyCol) from available_for_use
(évidemment il y a un peu plus car a) vous devrez coder pour les problèmes de concurrence - ne voulez pas que 2 copies de votre processus saisissent la même chosemin(KeyCol)
et b) vous 'll faudra supprimermin(KeyCol)
du tableau; cela devrait être relativement facile à coder, peut-être en tant que proc stocké, et peut être traité dans une autre Q&R si nécessaire)select min(KeyCol)
processus ne trouve aucune ligne disponible, vous pouvez lancer votre proc `` top off '' pour générer un nouveau lot de lignesAvec ces modifications proposées au modèle de données:
available_for_use
table pour vous assurer de ne jamais manquer de nouvelles valeursOui, le
available_for_use
tableau proposé n'est qu'un tableau de valeurs de «clé suivante» pré-générées; et oui, il y a un potentiel de contention lors de la saisie de la valeur `` suivante '', mais tout conflit a) est facilement résolu par une conception de table / d'index / de requête appropriée et b) va être mineur / de courte durée par rapport à la surcharge / retards avec l'idée actuelle de recherches répétées et forcées d'index.la source
n
clés (probablement 10 ou 20, pour le forcer à rechercher ce qui pourrait être des valeurs plus basses et plus souhaitables). Appréciez vraiment la réponse ici cependant, vous avez mis les pensées par écrit! :)KeyCol
valeurs disponibles ... oui, cela fonctionnerait aussi :-) et éliminerait évidemment la nécessité d'un changement de modèle de données ehKeyCol
gestionnaire distribué , et la nécessité de coder pour les violations potentielles de PK si 2 (ou plus) instances simultanées de l'application essaient d'utiliser la mêmeKeyCol
valeur ... beurk ... certainement plus facile avec un seul serveur middleware ou un db-centric solutionIl 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:
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 votreBINARY(64)
en unBIGINT
et il ne générera pas d'erreurs de conversion, mais le comportement n'est pas défini :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
: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:
Les résultats suggèrent que la valeur maximale à laquelle nous convertissons
BIGINT
est 102500672:Il y a 100 millions de lignes avec des valeurs qui correspondent à BIGINT comme prévu:
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: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 :
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.
la source
Voici une réponse qui ne fonctionnera probablement pas pour vous, mais je l'ajouterai quand même.
Même si BINARY (64) est dénombrable, il existe un faible soutien pour déterminer le successeur d'un élément. Étant donné que BIGINT semble être trop petit pour votre domaine, vous pouvez envisager d'utiliser un DECIMAL (38,0), qui semble être le plus grand type NUMBER dans SQL-server.
Trouver le premier écart est facile puisque nous pouvons construire le nombre que nous recherchons:
Une jointure de boucle imbriquée sur l'index pk devrait être suffisante pour trouver le premier élément disponible.
la source