web-dev-qa-db-fra.com

La vitesse d'insertion de SQLite ralentit à mesure que le nombre d'enregistrements augmente en raison d'un index

Question d'origine

Contexte

Il est bien connu que SQLite doit être affiné pour atteindre des vitesses d'insertion de l'ordre de 50 000 insertions/s. Il existe de nombreuses questions ici concernant les vitesses d'insertion lentes et une multitude de conseils et de références.

Il y a aussi prétend que SQLite peut gérer de grandes quantités de données , avec des rapports de 50+ Go ne causant aucun problème avec les bons paramètres.

J'ai suivi les conseils ici et ailleurs pour atteindre ces vitesses et je suis content des inserts 35k-45k/s. Le problème que j'ai est que tous les benchmarks ne montrent que des vitesses d'insertion rapides avec des enregistrements <1m. Ce que je vois, c'est que la vitesse d'insertion semble être inversement proportionnelle à la taille de la table .

Problème

Mon cas d'utilisation nécessite le stockage de 500 m à 1 b tuples ([x_id, y_id, z_id]) sur quelques années (1m lignes/jour) dans une table de liens. Les valeurs sont toutes des ID entiers compris entre 1 et 2 000 000. Il existe un seul index sur z_id.

Les performances sont excellentes pour les 10 premiers rangs, ~ 35 000 insertions/s, mais au moment où la table comporte ~ 20 millions de lignes, les performances commencent à en souffrir. Je vois maintenant environ 100 insertions/s.

La taille de la table n'est pas particulièrement grande. Avec 20 m de lignes, la taille du disque est d'environ 500 Mo.

Le projet est écrit en Perl.

Question

Est-ce la réalité des grandes tables dans SQLite ou y a-t-il des secrets pour maintenir des taux d'insertion élevés pour les tables avec> 10m de lignes?

Solutions de contournement connues que j'aimerais éviter si possible

  • Supprimez l'index, ajoutez les enregistrements et réindexez : c'est une bonne solution de contournement, mais cela ne fonctionne pas lorsque la base de données doit encore être utilisable lors des mises à jour. Cela ne fonctionnera pas pour rendre la base de données complètement inaccessible pendant x minutes/jour
  • Divisez le tableau en sous-tables/fichiers plus petits : Cela fonctionnera à court terme et je l'ai déjà expérimenté. Le problème est que je dois être en mesure de récupérer des données de tout l'historique lors de la requête, ce qui signifie que finalement j'atteindrai la limite de pièces jointes de 62 tables. Attacher, collecter des résultats dans une table temporaire et détacher des centaines de fois par demande semble être beaucoup de travail et de surcharge, mais je vais l'essayer s'il n'y a pas d'autres alternatives.
  • Définir SQLITE_FCNTL_CHUNK_SIZE : Je ne connais pas C (?!), Donc je préfère ne pas l'apprendre juste pour faire ça. Je ne vois cependant aucun moyen de définir ce paramètre à l'aide de Perl.

MISE À JOUR

Après suggestion de Tim qu'un index provoquait des temps d'insertion de plus en plus lents malgré les affirmations de SQLite selon lesquelles il est capable de gérer de grands ensembles de données, j'ai effectué une comparaison de référence avec les paramètres suivants:

  • lignes insérées: 14 millions
  • valider la taille du lot: 50000 enregistrements
  • cache_size pragma: 10 000
  • page_size pragma: 4 096
  • temp_store pragma: mémoire
  • journal_mode pragma: supprimer
  • synchronous pragma: désactivé

Dans mon projet, comme dans les résultats de référence ci-dessous, une table temporaire basée sur des fichiers est créée et la prise en charge intégrée de SQLite pour l'importation de données CSV est utilisée. La table temporaire est ensuite attachée à la base de données réceptrice et des ensembles de 50 000 lignes sont insérés avec un insert-select déclaration. Par conséquent, les heures d'insertion ne reflètent pas le fichier dans la base de données les heures d'insertion, mais plutôt table à table insérer la vitesse. La prise en compte du temps d'importation CSV réduirait les vitesses de 25 à 50% (estimation très approximative, l'importation des données CSV ne prend pas longtemps).

Clairement, avoir un index ralentit la vitesse d'insertion à mesure que la taille de la table augmente.

Plot of SQLite insert speed and table size

Il est assez clair à partir des données ci-dessus que la bonne réponse peut être attribuée à la réponse de Tim plutôt qu'aux affirmations selon lesquelles SQLite ne peut tout simplement pas le gérer. Clairement, il peut gérer de grands ensembles de données si indexer cet ensemble de données ne fait pas partie de votre cas d'utilisation. J'utilise SQLite pour cela, en tant que backend pour un système de journalisation, depuis un certain temps, qui n'a pas besoin d'être indexé, donc j'étais assez surpris du ralentissement que j'ai connu.

Conclusion

Si quelqu'un se retrouve à vouloir stocker une grande quantité de données en utilisant SQLite et le faire indexer, en utilisant des fragments peut être la réponse . J'ai finalement décidé d'utiliser les trois premiers caractères d'un hachage MD5, une colonne unique dans z pour déterminer l'affectation à l'une des 4 096 bases de données. Étant donné que mon cas d'utilisation est principalement de nature archivistique, le schéma ne changera pas et les requêtes ne nécessiteront jamais la marche des fragments. Il y a une limite à la taille de la base de données car les données extrêmement anciennes seront réduites et éventuellement supprimées, donc cette combinaison de partitionnement, de paramètres de pragma et même d'une normalisation de de (- === -) me donne un bel équilibre qui, sur la base de l'analyse comparative ci-dessus, maintiendra une vitesse d'insertion d'au moins 10k inserts/seconde.

59
user918938

Si vous avez besoin de trouver un z_id et le x_ids et y_ids lié à celui-ci (par opposition à la sélection rapide d'une plage de z_ids), vous pouvez rechercher une base de données relationnelle imbriquée non indexée qui vous permettrait de trouver instantanément votre chemin vers un z_id afin d'obtenir son y_ids et x_ids - sans la surcharge d'indexation et les performances dégradées concomitantes lors des insertions à mesure que l'index augmente. Afin d'éviter l'agglutination (alias les collisions de bucket), choisissez un algorithme de hachage de clé qui accorde le plus de poids aux chiffres de z_id avec la plus grande variation (pondéré à droite).

P.S. Une base de données qui utilise un arbre b peut au premier abord apparaître plus rapidement qu'une base de données qui utilise le hachage linéaire, par exemple, mais les performances d'insertion resteront au même niveau que le hachage linéaire à mesure que les performances de l'arbre b commenceront à se dégrader.

P.P.S. Pour répondre à la question de @ kawing-chiu: la principale caractéristique pertinente ici est qu'une telle base de données repose sur des tables dites "clairsemées" dans lesquelles l'emplacement physique d'un enregistrement est déterminé par un algorithme de hachage qui prend la clé d'enregistrement en entrée. Cette approche permet une recherche directement à l'emplacement de l'enregistrement dans la table sans l'intermédiaire d'un index. Comme il n'est pas nécessaire de parcourir les index ou de rééquilibrer les index, les temps d'insertion restent constants à mesure que la table devient plus dense. Avec un arbre b, en revanche, les temps d'insertion se dégradent à mesure que l'arbre d'index se développe. OLTP les applications avec un grand nombre d'insertions simultanées peuvent bénéficier d'une telle approche de table clairsemée. Les enregistrements sont dispersés dans toute la table. L'inconvénient des enregistrements étant dispersés à travers la "toundra" de la table clairsemée est que la collecte de grands ensembles d'enregistrements qui ont une valeur en commun, comme un code postal, peut être plus lente. L'approche par table clairsemée hachée est optimisée pour insérer et récupérer des enregistrements individuels et pour récupérer réseaux d'enregistrements associés, pas de grands ensembles d'enregistrements ayant une valeur de champ en commun.

Une base de données relationnelle imbriquée est celle qui autorise les tuples dans une colonne d'une ligne.

13
Tim

Grande question et suivi très intéressant!

Je voudrais juste faire une brève remarque: vous avez mentionné que casser le tableau en sous-tables/fichiers plus petits et les attacher plus tard n'est pas une option car vous atteindrez rapidement la limite stricte de 62 bases de données attachées. Bien que cela soit complètement vrai, je ne pense pas que vous ayez envisagé une option à mi-chemin: partager les données en plusieurs tables mais continuez à utiliser la même base de données unique (fichier).


J'ai fait un benchmark très grossier juste pour m'assurer que ma suggestion a vraiment un impact sur les performances.

Schéma:

CREATE TABLE IF NOT EXISTS "test_$i"
(
    "i" integer NOT NULL,
    "md5" text(32) NOT NULL
);

Données - 2 millions de lignes:

  • i = 1..2,000,000
  • md5 = résumé hexadécimal md5 de i

Chaque transaction = 50 000 INSERTs.


Bases de données: 1; Tableaux: 1; Index: 0

0..50000 records inserted in 1.87 seconds
50000..100000 records inserted in 1.92 seconds
100000..150000 records inserted in 1.97 seconds
150000..200000 records inserted in 1.99 seconds
200000..250000 records inserted in 2.19 seconds
250000..300000 records inserted in 1.94 seconds
300000..350000 records inserted in 1.94 seconds
350000..400000 records inserted in 1.94 seconds
400000..450000 records inserted in 1.94 seconds
450000..500000 records inserted in 2.50 seconds
500000..550000 records inserted in 1.94 seconds
550000..600000 records inserted in 1.94 seconds
600000..650000 records inserted in 1.93 seconds
650000..700000 records inserted in 1.94 seconds
700000..750000 records inserted in 1.94 seconds
750000..800000 records inserted in 1.94 seconds
800000..850000 records inserted in 1.93 seconds
850000..900000 records inserted in 1.95 seconds
900000..950000 records inserted in 1.94 seconds
950000..1000000 records inserted in 1.94 seconds
1000000..1050000 records inserted in 1.95 seconds
1050000..1100000 records inserted in 1.95 seconds
1100000..1150000 records inserted in 1.95 seconds
1150000..1200000 records inserted in 1.95 seconds
1200000..1250000 records inserted in 1.96 seconds
1250000..1300000 records inserted in 1.98 seconds
1300000..1350000 records inserted in 1.95 seconds
1350000..1400000 records inserted in 1.95 seconds
1400000..1450000 records inserted in 1.95 seconds
1450000..1500000 records inserted in 1.95 seconds
1500000..1550000 records inserted in 1.95 seconds
1550000..1600000 records inserted in 1.95 seconds
1600000..1650000 records inserted in 1.95 seconds
1650000..1700000 records inserted in 1.96 seconds
1700000..1750000 records inserted in 1.95 seconds
1750000..1800000 records inserted in 1.95 seconds
1800000..1850000 records inserted in 1.94 seconds
1850000..1900000 records inserted in 1.95 seconds
1900000..1950000 records inserted in 1.95 seconds
1950000..2000000 records inserted in 1.95 seconds

Taille du fichier de base de données: 89,2 MiB.


Bases de données: 1; Tableaux: 1; Index: 1 (md5)

0..50000 records inserted in 2.90 seconds
50000..100000 records inserted in 11.64 seconds
100000..150000 records inserted in 10.85 seconds
150000..200000 records inserted in 10.62 seconds
200000..250000 records inserted in 11.28 seconds
250000..300000 records inserted in 12.09 seconds
300000..350000 records inserted in 10.60 seconds
350000..400000 records inserted in 12.25 seconds
400000..450000 records inserted in 13.83 seconds
450000..500000 records inserted in 14.48 seconds
500000..550000 records inserted in 11.08 seconds
550000..600000 records inserted in 10.72 seconds
600000..650000 records inserted in 14.99 seconds
650000..700000 records inserted in 10.85 seconds
700000..750000 records inserted in 11.25 seconds
750000..800000 records inserted in 17.68 seconds
800000..850000 records inserted in 14.44 seconds
850000..900000 records inserted in 19.46 seconds
900000..950000 records inserted in 16.41 seconds
950000..1000000 records inserted in 22.41 seconds
1000000..1050000 records inserted in 24.68 seconds
1050000..1100000 records inserted in 28.12 seconds
1100000..1150000 records inserted in 26.85 seconds
1150000..1200000 records inserted in 28.57 seconds
1200000..1250000 records inserted in 29.17 seconds
1250000..1300000 records inserted in 36.99 seconds
1300000..1350000 records inserted in 30.66 seconds
1350000..1400000 records inserted in 32.06 seconds
1400000..1450000 records inserted in 33.14 seconds
1450000..1500000 records inserted in 47.74 seconds
1500000..1550000 records inserted in 34.51 seconds
1550000..1600000 records inserted in 39.16 seconds
1600000..1650000 records inserted in 37.69 seconds
1650000..1700000 records inserted in 37.82 seconds
1700000..1750000 records inserted in 41.43 seconds
1750000..1800000 records inserted in 49.58 seconds
1800000..1850000 records inserted in 44.08 seconds
1850000..1900000 records inserted in 57.17 seconds
1900000..1950000 records inserted in 50.04 seconds
1950000..2000000 records inserted in 42.15 seconds

Taille du fichier de base de données: 181,1 MiB.


Bases de données: 1; Tableaux: 20 (un pour 100 000 enregistrements); Index: 1 (md5)

0..50000 records inserted in 2.91 seconds
50000..100000 records inserted in 10.30 seconds
100000..150000 records inserted in 10.85 seconds
150000..200000 records inserted in 10.45 seconds
200000..250000 records inserted in 10.11 seconds
250000..300000 records inserted in 11.04 seconds
300000..350000 records inserted in 10.25 seconds
350000..400000 records inserted in 10.36 seconds
400000..450000 records inserted in 11.48 seconds
450000..500000 records inserted in 10.97 seconds
500000..550000 records inserted in 10.86 seconds
550000..600000 records inserted in 10.35 seconds
600000..650000 records inserted in 10.77 seconds
650000..700000 records inserted in 10.62 seconds
700000..750000 records inserted in 10.57 seconds
750000..800000 records inserted in 11.13 seconds
800000..850000 records inserted in 10.44 seconds
850000..900000 records inserted in 10.40 seconds
900000..950000 records inserted in 10.70 seconds
950000..1000000 records inserted in 10.53 seconds
1000000..1050000 records inserted in 10.98 seconds
1050000..1100000 records inserted in 11.56 seconds
1100000..1150000 records inserted in 10.66 seconds
1150000..1200000 records inserted in 10.38 seconds
1200000..1250000 records inserted in 10.24 seconds
1250000..1300000 records inserted in 10.80 seconds
1300000..1350000 records inserted in 10.85 seconds
1350000..1400000 records inserted in 10.46 seconds
1400000..1450000 records inserted in 10.25 seconds
1450000..1500000 records inserted in 10.98 seconds
1500000..1550000 records inserted in 10.15 seconds
1550000..1600000 records inserted in 11.81 seconds
1600000..1650000 records inserted in 10.80 seconds
1650000..1700000 records inserted in 11.06 seconds
1700000..1750000 records inserted in 10.24 seconds
1750000..1800000 records inserted in 10.57 seconds
1800000..1850000 records inserted in 11.54 seconds
1850000..1900000 records inserted in 10.80 seconds
1900000..1950000 records inserted in 11.07 seconds
1950000..2000000 records inserted in 13.27 seconds

Taille du fichier de base de données: 180,1 MiB.


Comme vous pouvez le voir, la vitesse d'insertion reste à peu près constante si vous divisez les données en plusieurs tables.

7
Alix Axel

Malheureusement, je dirais que c'est une limitation des grandes tables dans SQLite. C'est non conç pour fonctionner sur des ensembles de données à grande échelle ou à grand volume. Bien que je comprenne que cela peut augmenter considérablement la complexité du projet, vous feriez probablement mieux de rechercher des solutions de base de données plus sophistiquées et adaptées à vos besoins.

De tout ce que vous avez lié, il semble que la taille de la table et la vitesse d'accès soient un compromis direct. Je ne peux pas avoir les deux.

2
Michael Pratt

Dans mon projet, je n'ai pas pu partager la base de données, car elle est indexée sur différentes colonnes. Pour accélérer les insertions, j'ai mis la base de données lors de la création sur/dev/shm (= linux ramdisk), puis je la copie sur le disque local. Cela ne fonctionne évidemment bien que pour une base de données à écriture unique et à lecture multiple.

1
Max

Je soupçonne que la collision de la valeur de hachage de l'index ralentit la vitesse d'insertion.

Lorsque nous avons plusieurs lignes dans une même table, la collision de valeurs de hachage de colonne indexée se produit plus fréquemment. Cela signifie que le moteur Sqlite doit calculer la valeur de hachage deux ou trois fois, voire quatre fois, pour obtenir une valeur de hachage différente.

Donc je suppose que c'est la cause première de la lenteur d'insertion SQLite lorsque la table a plusieurs lignes.

Ce point pourrait expliquer pourquoi l'utilisation de fragments pourrait éviter ce problème. Qui est un vrai expert dans le domaine SQLite pour confirmer ou infirmer mon point ici?

0
Clock ZHONG