Becnhmark-Test für Arme (SQLite-Geschwindigkeit)

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

execute() übergibst du SQL oder was auch immer SQLAlchemy zu SQL konvertieren kann und Tupel mit den Werten die eingesetzt werden sollen. In diesem Fall heisst dass das wir die Namen die wir von generate_names() bekommen in ein Tupel stecken müssen und diese Tupel übergeben wir dann als Argumente an execute(). Das ist was hier passiert. Die GC erzeugt die Tupel und mit dem Sternchen packen wir die GC aus damit jedes Tupel als einzelnes Argument übergeben wird.
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

@DasIch, @Sophus: das * sorgt dafür, dass die durch die GC erzeugten Elemente als Parameter an execute übergeben werden. Die liegen also gleichzeitig im Speicher. Daher ist es ja auch besser, nicht irgendwelche undokumentierten Funktionalitäten zu nehmen, sondern gleich die dafür vorgesehene Funktion executemany:

Code: Alles auswählen

connection.executemany(
    'INSERT INTO test_table (generated_name) VALUES (?)',
    ((name, ) for name in islice(generate_names(), number_of_records))
)
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Connection hat keine executemany Methode. Diese Funktionalität ist dokumentiert.
BlackJack

Der ”Fehler” hier ist SQLAlchemy mit so simplem SQL als Zeichenkette zu verwenden. Das verwirrt offenbar sehr viele Leute, weil man das eigentlich nicht macht. Ich bin am Anfang ja auch darauf reingefallen. :-)
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Code: Alles auswählen

#!/usr/bin/env python3.5
import os
from time import perf_counter
from functools import partial
from itertools import count, islice

from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String


DATABASE_PATH = 'test.sqlite'
DATABASE_URI = 'sqlite:///' + DATABASE_PATH


metadata = MetaData()

test_table = Table('test_table', metadata,
    Column('id', Integer, primary_key=True),
    Column('generated_name', String(100), unique=True)
)


def remove_file(filepath):
    try:
        os.remove(filepath)
    except FileNotFoundError:
        pass
    except:
        raise


def input(prompt, type=None):
    while True:
        raw_answer = __builtins__.input(prompt)
        if type is None:
            return raw_answer
        try:
            return type(raw_answer)
        except ValueError:
            print('{!r} is not a {}'.format(raw_answer, type))


def benchmark(function):
    start = perf_counter()
    function()
    end = perf_counter()
    return end - start


def generate_names():
    for i in count():
        yield 'Name #{}'.format(i)


def insert_records(connection, number_of_records):
    values = (
        {'generated_name': name}
        for name in islice(generate_names(), number_of_records)
    )
    connection.execute(test_table.insert(), *values)


def main():
    remove_file(DATABASE_PATH)
    engine = create_engine(DATABASE_URI)

    with engine.connect() as connection:
        metadata.create_all(connection)
        number_of_records = input('How many records should be inserted? ', type=int)
        duration = benchmark(partial(insert_records, connection, number_of_records))
        print('Duration: {}s'.format(duration))


if __name__ == '__main__':
    main()
Das was man eigentlich macht dauert aus irgendeinem Grund nahezu doppelt so lange. Einen kleinen konstanten Overhead kann ich nachvollziehen aber ich versteh nicht wo die Zeit hier verloren geht. Irgendwer eine Idee was hier schief geht?
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

@DasIch: ich war verwirrt, weil Du statt der austauschbaren DB-API2 die nicht-konformen Erweiterungen von sqlite3 benutzt. Das macht das wechseln der Datenbank unnötig kompliziert. Arbeite mit Cursor-Objekten und executemany, das zusätzlich den Vorteil hat, dass der Generator auch wirklich benutzt wird.
BlackJack

@Sirius3: Du bist immer noch verwirrt, genau wie ich am Anfang: Es wird hier die ganze Zeit und von allen Beteiligten die Code zeigen SQLAlchemy verwendet!
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack @Sirius3: Ich weiß, die meisten verbinden SQLAlchemy mit ORM. Aber es ist genauso legitim SQLAlchemy mit DDL zu benutzen. ORM ist einfach bedeutend langsamer als DDL.
BlackJack

@Sophus: Das ist nicht das Problem, sondern das niemand SQLAlchemy mit so simplem handgeschriebenen SQL in Zeichenketten verbindet. Denn dann macht SQLAlchemy keinen Sinn. (Du benutzt da übrigens mehr als nur den DDL-Teil von SQL.)

Und was die Geschwindigkeit vom ORM angeht: Die ist *ausreichend*. Oder meinst Du jemand kann schneller Filme in Deine Datenbank über die Qt-Oberfläche eintippen als das ORM sie in die Datenbank schreiben kann? Du machst Dir schon wieder mal Gedanken über Probleme die nicht existieren.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Ich glaube, es muss ein Missverständnis sein. In meinem Projekt verwende ich selbstverständlich ORM - wobei ich diesen Hype um ORM nicht sonderlich gut finde. Schließlich ist es eher bereichernd zu wissen, wie eine DDL aussieht, um zu wissen, was die Datenbank da macht. Dies wird ja bei einem ORM "versteckt". Aber das ist ein anderes Thema. Ich wollte eher ein kleines Skript schreiben, um die Geschwindigkeit von SQLite zu testen. Und da hielt ich ORM unangebracht.
Antworten