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.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo Leute,

da in vielen Foren und auf vielen Seiten gesagt wird, dass SQLite schneller als MySQL sei habe ich ein kleines Skript geschrieben, um die Geschwindigkeit der SQLite-Datenbank zu testen. Den lauffähigen Quelltext findet ihr unten aufgeführt, so wie meine Laptop-Angaben. Dazu werdet ihr weiter unten die Zeit für die INSTERTS sehen. Vor diesem Hintergrund möchte ich euch fragen, ob die Zeiten akzeptable sind, oder ob es an meinem Quelltext liegt, dass die automatisierte Einträge so "langsam" sind? Ich habe mich bewusst bei diesem Test auf ORM verzichtet, da das ORM-Konzept wesentlich langsamer ist, als wenn man mit DDL (Data Definition Language) arbeitet. Mir ist durchaus bewusst, dass man in Sachen Geschwindigkeit bei Python an falscher Adresse ist. Mir geht es viel mehr darum, ob die Zeit-Werte, die gemessen wurden, in Ordnung sind, oder ob es noch Optimierungsbedarf an meinem Quelltext gibt? Den Test habe ich bei 10.000.000 INSERTS abgebrochen. 2 Tage haben mir gereicht. Für mein Gefühl dauerte es einfach zu lange.

Laptop-Leistung
Betriebssystem: Windows 7 Home
2x CPU: 2 GHz (3 MB Cache)
System: 64 Bit
Arbeitsspeicher: 4 GB
Quelltext:

Code: Alles auswählen

#!/usr/bin/env python

import os
import sys
import time
from datetime import datetime

from sqlalchemy import create_engine
from sqlalchemy import exc

BASE_PATH = os.path.dirname(os.path.abspath("__file__"))

# default db engine
DB_URI = 'sqlite:///test_database.sqlite'
 
def insert_record(range_int):
    conn = connect_database(DB_URI)
    try:
        for x in (range(int(range_int))):
            result = conn.execute("INSERT INTO test_table (some_row) VALUES ('{var_1}')".format(var_1="TestName %d" % (x)))
    except exc.IntegrityError as InErr:
        print "InErr", InErr
    conn.close()
    
def connect_database(database_path):
    engine = create_engine(database_path, echo=False)
    conn = engine.connect()
    return conn

def create_table():
    # Geneeal information
    table_name = "test_table"
    id_row = table_name+"_id"
    some_name = "some_row"

    # get connections-string
    conn = connect_database(DB_URI)
    
    createing_table = """CREATE TABLE IF NOT EXISTS {name_table} (
    {profile_name_id} INTEGER PRIMARY KEY AUTOINCREMENT, 
    {some_name} VARCHAR(100), 

     
    UNIQUE ({some_name}))""".format(name_table=table_name,
                                     profile_name_id=id_row,
                                     some_name=some_name)

    conn.execute(createing_table)
    conn.close()

def main():
    # First always delete the data base for this testing
    os.remove("test_database.sqlite")
    
    # Second create a new database with empty tables, rows and columns
    create_table()

    # Interactive from user
    enter_range = raw_input("How many records to be inserted? (only numbers): ")

    # start to stopp the time
    start_time = datetime.now()

    # Call function to insert data X times items
    insert_record(enter_range)

    # end with to stopp the time
    end_time = datetime.now()

    # Show how much time has been elapsed.
    print('Duration: {}'.format(end_time - start_time))
    
if __name__ == '__main__':
    main()
Inserts: 10 = Duration: 0:00:00.074000
---------------------------------------------------------
Inserts: 100 = Duration: 0:00:01.174000
---------------------------------------------------------
Inserts: 1.000 = Duration: 0:00:11.701000
---------------------------------------------------------
Inserts: 10.000 = Duration: 0:02:14.843000
---------------------------------------------------------
Inserts: 100.000 = Duration: 0:21:02.627000
---------------------------------------------------------
Inserts: 1.000.000 = Duration: 4:09:03.760000
---------------------------------------------------------
Inserts: 10.000.000 = Duration: 2 days, 16:20:43.034000
BlackJack

@Sophus: Das dauert echt sehr lange dafür das Du da *nichts* in die Datenbank eingefügt hat. *Eine* Transaktion mit 10 Millionen Datensätzen die Du dann nicht commitest. Und mit in SQL-Zeichenketten hineinformatierte Werte. Und mit `execute()` anstelle sich dem hier doch sehr anbietenden `executemany()`. Und mit AUTOINCREMENT, wovon die SQLite-Dokumentation aus Geschwindigkeitsgründen abrät.

Und tolle Namen. `insert_record()` das *viele* Datensätze einfügt. `range_int` was an eine Zeichenkette gebunden ist. `range()` das erst einmal tatsächlich eine Liste mit Zahlen erzeugt. `some_row` als Namen für eine *Spalte*.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Sophus hat geschrieben:da in vielen Foren und auf vielen Seiten gesagt wird, dass SQLite schneller als MySQL
:roll: Ein Ferrari ist auch schneller als ein LKW. Trotzdem werde ich nicht für einen Umzug einen Ferrari nehmen damit es schneller geht.

SQLite und MySQL sind zwar beides relationelle Datenbanken aber damit hören die Gemeinsamkeiten auf. Sie sind unterschiedlich genug dass es eigentlich kein Szenario gibt wo du sinnvoll SQLite durch MySQL oder MySQL durch SQLite ersetzen könntest.

Selbst wenn SQLite und MySQL das gleiche Problem lösen würden, MySQL ist so schlecht dass du es ohnehin nicht nutzen solltest.

In Anbetracht deines Codes würde ich dir auch empfehlen deinen eigenen Code näher zu betrachten. Wenn du Geschwindigkeitsprobleme hast, werden die höchstwahrscheinlich da liegen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BLackJack: Die Namen sind wirklich nicht gut gewählt. Und ich habe die commit()-Methode nicht aufgerufen, da die Datensätze tatsächlich abgespeichert werden.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@DasIch: Wenn ich dich richtig verstehe, hast du Mängel in meinem Code entdeckt? Welche wären es? (mal von schlechten Namen abgesehen).

EDIT: Und worauf berufst du dich, wenn du sagst, das MySQL sehr schlecht sei? Das würde mich allgemein interessieren. Dies soll keine Grundsatzdiskussion werden.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

insert_record
  • Ruft connect_database selbst auf, was die Funktion untestbar macht.
  • Nutzt kein with statement um die Verbindung sauber zu schliessen.
  • Wieso ist range_int kein int?
  • Mixt unterschiedliche Wege zur String Formatierung. Es ist schlimm genug dass es schon mehrere gibt aber wenigstens im eigenen Projekt, definitiv aber in einer Funktion sollte man bei einer Variante bleiben.
  • Du formattierst das SQL statt die Parameter sauber zu übergeben. Das ist grundsätzlich wie SQL Injection zustande kommt. Das kann zwar hier nicht passieren aber man sollte sich gar nicht erst angewöhnen so zu programmieren. Es bremst hier außerdem die Datenbank aus.
  • Die Ausnahmebehandlung... Wenn ein Anfänger sowas macht dann erklärt man wieso man sowas nicht macht und die Person lernt besseren Code zu schreiben. Du hast allerdings schon hunderte Posts hier geschrieben, du solltest eigentlich wissen dass das Scheisse ist und wieso.
  • Du machst keinen Commit oder überhaupt irgendeine lesende Operation, die Datenbank muss also nahezu gar nichts tun, tut sie denke ich auch nicht. Dein ganzer Benchmark ist also wertlos weil nichts wirklich gemessen wird.
create_table
  • Kommentare können hilfreich sein, in diesem Fall sind sie es nicht. Bei "get connections-string" frage ich mich was ein "connections-string" sein soll, ich bin mir aber ziemlich sicher dass der Kommentar falsch ist.
  • Auch hier wird wieder connect_database aufgerufen.
  • With statement wäre auch hier angebracht.
  • Es ist creating.
  • Es gibt überhaupt keine Grund dafür den String hier zu formattieren. Wofür das ganze? Auf den Verdacht hin du sparst ein paar Sekunden falls du den Code jemals ändern solltest?
main
  • Wir haben wieder Kommentare die manchmal sinnlos und manchmal verwirrend sind.
  • os.remove liefert eine Exception wenn die Datei nicht existiert.
  • Der Name range impliziert mehr als eine Zahl.
  • Input Validierung? Zumindest mal Konvertierung in eine Zahl an der Stelle an der es sich gehört?
  • datetime bzw. überhaupt die Zeit wie sie uns eine Uhr zeigt ist nicht monoton, sie kann stehen bleiben, zurück- oder vorgehen. Du kannst also Ergebnisse bekommen die nichts damit zu tun haben wieviel Zeit tatsächlich vergangen ist.
Das Internet ist voll mit Erklärungen wieso und was alles an MySQL schlecht ist. Worauf es im wesentlichen hinausläuft ist das MySQL dazu tendiert Fehler zu ignorieren statt sich darüber zu beschweren und deswegen Daten kaputt macht. Migrationen sind ein großes Problem bei MySQL was die Performance angeht. MySQL lässt außerdem so einiges zu wünschen übrig was die Optimierung von Queries angeht.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@DasIch: Zunächst ein Dankeschön.

Es ist ziemlich spät, aber bevor ich ins Bett gehe, möchte ich den geänderten Quelltext präsentieren. Ich habe hier versucht DasIchs ausführliche Kritik zu berücksichtigen. Ich hoffe, es ist mir einigermaßen gelungen. Den Quelltext seht ihr weiter unten. Bei einem erneuten Test (ich ging aus Zeitgründen nur bis 10.000) habe ich bezüglich der Zeitmessung keine signifikante Veränderungen gesehen.

Hinweis an BlackJack und DasIch: Das Objekt conn im Context Manager der insert_record()-Funktion hat keine commit()-Methode. Es ist auch gar nicht nötig, denn die Datensätze werden anstandslos in die Datenbank hinterlegt. Um dies zu überprüfen bin ich gleich zwei Wege gegangen. Erstens habe ich das echo mal auf True gesetzt und da liest man unter anderem folgendes: 2016-02-16 04:48:32,375 INFO sqlalchemy.engine.base.Engine COMMIT. Und der zweite Weg war, dass ich über den SQLManager nachgesehen habe, ob die Datensätze tatsächlich in die SQLite-Datenbank persistiert wurden. In beiden Fällen erfolgreich.

Code: Alles auswählen

#!/usr/bin/env python

import os
import errno
import sys
import time
from datetime import datetime

from sqlalchemy import create_engine
from sqlalchemy import exc

def insert_record(range_int, engine_obj):
    
    # start to stopp the time
    start_time = datetime.now()
    
    with engine_obj.connect() as conn:
        for x in (range(range_int)):
            result = conn.execute("INSERT INTO test_table (generated_name) VALUES ('{var_1}')".format(var_1="TestName {}".format(x)))

    # end with to stopp the time
    end_time = datetime.now()

    return end_time - start_time
    
def get_engine(database_path):
    return create_engine(database_path, echo=False)

def create_table(engine_obj):
   
    creating_table = """CREATE TABLE IF NOT EXISTS test_table (
                        id INTEGER PRIMARY KEY AUTOINCREMENT, 
                        generated_name VARCHAR(100),
                        UNIQUE (generated_name))"""

    with engine_obj.connect() as conn:
        conn.execute(creating_table)

def remove_file(file_name):

    try:
        os.remove(file_name)
    except OSError as e: 
        if e.errno != errno.ENOENT:
            raise

def main():

    DB_URI = 'sqlite:///test_database.sqlite'

    remove_file("test_database.sqlite")

    engine = get_engine(DB_URI)
    
    create_table(engine)

    enter_amount = raw_input("How many records to be inserted? (only numbers): ")
    
    try:
        result = insert_record(int(enter_amount), engine)
        print "Duration:", result
    except ValueError:
        print 'Please enter an integer'
        
if __name__ == '__main__':
    main()
BlackJack

@sophus: Okay, beim `commit()` hast Du uns anscheinend beide damit ”erwischt” das wir bei „ich nehme kein ORM“ und SQL-Anweisung als Zeichenkette gedacht haben Du nimmst auch tatsächlich kein SQLAlchemy, weil man da für so etwas natürlich kein SQL als Zeichenkette schreiben würde. Wozu verwendest Du es denn wenn Du es am Ende gar nicht wirklich verwendest? Dann ist es also genau umgekehrt: Du machst da unnötigeweise implizit bei jedem einfügen ein `commit()` weil SQLAlchemy das macht. Das ist bei grossen Datenmengen die auf einen Schlag in eine Datenbank geschrieben werden genau so unsinnig wie es nur einmal ganz am Ende zu machen. Wenn man solche „Batch“-Verarbeitung macht, würde man wenn man es Optimieren möchte sowieso ein paar Pragmas verwenden. Allerdings würde ich das auch erst empfehlen wenn man tatsächlich ein Programm hat welches objektiv zu langsam ist. Wir beschäftigen uns hier IMHO schon wieder zu sehr mit einem von Dir *gefühlten Problem*.

Du formatierst immer noch den Wert als Zeichenkette in das SQL und machst immer noch für jeden einzelnen Datensatz ein eigenes `execute()`. Das würde man hier so nicht machen.

Die Namenszusätze `_int` und `_obj` sind überflüssig. Bei `range_int` wäre beim weglassen des Suffix der Name `range` unpassend und würde die eingebaute Funktion verdecken. `insert_record()` ist immer noch Singular, obwohl die Funktion mehr als einen Datensatz einfügt.

Die Ausnahmebehandlung in der `main()`-Funktion ist im Grunde falsch, denn das ``try`` deckt *deutlich* mehr ab als die Umwandlung der Eingabe in eine ganze Zahl, das heisst der `ValueError` muss nicht zwingend von dieser Operation kommen sondern kann auch irgendwo von `insert_record()` oder einer dort aufgerufenen Funktion oder Methode kommen und dann wäre die Ausgabe an den Benutzer falsch.

Vielleicht noch ein Wort zur Aussagekraft von dem Benchmark: Ich würde als erstes mal den Lauf mit Einem vergleichen bei dem kein UNIQUE auf die `generated_name`-Spalte gelegt wird. Denn so wie Du diesen Wert erzeugst, werden reale Werte wohl kaum aussehen, und ich könnte mir vorstellen, dass wenn der Index von der Datenbank als Baum organisiert ist, dieser zu einer Liste oder zumindest in diese Richtung degenerieren kann bei den Werten, was dann wirklich schlechte Laufzeiten zur Folge hätte.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Rein gedanklich: Jedesmal, wenn ein Datensatz (TestName 0, TestName 1 etc.) im Range generiert wurde, muss der generierte Datensatz ja irgendwo hin. Und die execute()-Methode ist hier die einzige Methode die ich verwende. Wo sollte ich deiner Meinung nach dies Methode hinpacken? Oder anders gefragt: Was soll ich bei jedem Durchlauf im Range mit den generierten Datensätzen machen?

Und das die Datensätze so in der Praxis nicht aussehen werden, ist allen bewusst. Selbst wenn ich UNIQUE weg lasse, und anstatt TestName 0, TestName 1 etc nun TestName mache, dann sind alle Datensätze gleich, denn sie alle heißen dann TestName. Und so werden die Datensätze in der Praxis auch nicht aussehen.
BlackJack

@Sophus: Es geht nicht um das wo der `execute()`-Methode, sondern um das wie. Und das überhaupt. Aber wie man das macht und welche andere Methode schreibe ich jetzt bestimmt nicht *noch mal*.

Natürlich sehen die Daten in der Praxis so nicht aus, aber es macht schon einen Unterschied ob man sich für Tests Zufallsdaten hernimmt, oder welche die denen in der Praxis wenigstens ähnlich sind, oder ob man Constraints und Daten verwendet, die das ganze wahrscheinlich extrem langsam machen. Falls meine Vermutung mit dem degenerierten Suchbaum zutreffen sollte, verwendest Du Daten die schlechter kaum sein könnten.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Auf Anraten BlackJacks habe ich einiges im vorherigen Quelltext geändert. Der Quelltext wird weiter unten vorgestellt. Ich habe diesmal wieder einen Test durchgeführt, jedoch bin ich diesmal nur bis 1.000.000 gegangen. Ich hatte keine Lust noch einen weiteren Tag zu warten. Wir sehen nachfolgend zwei Tests. Im ersten Test war eine Spalte mit UNIQUE versehen und im Range wurden nummerierte Datensätze erzeugt. Im zweiten Test habe ich beide Faktoren weg gelassen. Wenn man beide Tests miteinander vergleicht, sehen wir einen Unterschied. BlackJack hatte also Recht. Hier konnte man einiges an Leistung einsparen und die Geschwindigkeit erhöhen.
First Test

Inserts: 10 = Duration: 0:00:00.074000
Inserts: 100 = Duration: 0:00:01.174000
Inserts: 1.000 = Duration: 0:00:11.701000
Inserts: 10.000 = Duration: 0:02:14.843000
Inserts: 100.000 = Duration: 0:21:02.627000
Inserts: 1.000.000 = Duration: 4:09:03.760000
Inserts: 10.000.000 = Duration: 2 days, 16:20:43.034000
----------------------------------------------------------------
----------------------------------------------------------------

Second Test

Inserts: 10 = Duration: 0:00:00.059000
Inserts: 100 = Duration: 0:00:00.987000
Inserts: 1.000 = Duration: 0:00:09.188000
Inserts: 10.000 = Duration: 0:01:36.985000
Inserts: 100.000 = Duration: 0:15:53.621000
Inserts: 1.000.000 = Duration: 3:01:54.254000
Inserts: 10.000.000 = ------

Code: Alles auswählen

#!/usr/bin/env python

import os
import errno
import sys
import time
from datetime import datetime

from sqlalchemy import create_engine
from sqlalchemy import exc

def insert_record(range_int, engine_obj):
    
    # start to stopp the time
    start_time = datetime.now()
    
    with engine_obj.connect() as conn:
        for x in (range(range_int)):
            result = conn.execute("INSERT INTO test_table (generated_name) VALUES ('TestName')")

    # end with to stopp the time
    end_time = datetime.now()

    return end_time - start_time
    
def get_engine(database_path):
    return create_engine(database_path, echo=False)

def create_table(engine_obj):
   
    creating_table = """CREATE TABLE IF NOT EXISTS test_table (
                        id INTEGER PRIMARY KEY AUTOINCREMENT, 
                        generated_name VARCHAR(100))"""

    with engine_obj.connect() as conn:
        conn.execute(creating_table)

def remove_file(file_name):

    try:
        os.remove(file_name)
    except (OSError) as e: 
        if e.errno != errno.ENOENT:
            raise

def main():

    DB_URI = 'sqlite:///test_database.sqlite'

    remove_file("test_database.sqlite")

    engine = get_engine(DB_URI)
    
    create_table(engine)

    enter_amount = raw_input("How many records to be inserted? (only numbers): ")
    
    try:
        result = int(enter_amount)
    except ValueError:
        print 'Please enter an integer'

    try:
        result = insert_record(int(enter_amount), engine)
        print "Duration:", result
    except ValueError:
        pass
        
if __name__ == '__main__':
    main()
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


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


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


def create_table(connection):
    connection.execute("""
    CREATE TABLE IF NOT EXISTS test_table (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        generated_name VARCHAR(100) UNIQUE
    )
    """)


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):
    connection.execute(
        'INSERT INTO test_table (generated_name) VALUES (?)',
        *((name, ) for name in islice(generate_names(), number_of_records))
    )


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

    with engine.connect() as connection:
        create_table(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()

Code: Alles auswählen

λ python bench.py 
How many records should be inserted? 1000000
Duration: 8.490176044986583s
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@DasIch: Verdammt, ist dein Quelltext-Design optimiert worden. Ist es normal, dass ich mich jetzt echt dumm vorkomme? Aber so lerne ich wenigstens was hinzu. Aber ich möchte deinen Quelltext auch verstehen, daher werde ich mal versuchen, dass so wieder zu geben, wie ich es verstehe - nur einige Stellen am Quelltext. Und im Anschluss habe ich dann ein paar Fragen.

main():
Was mir hier sehr neu ist, ist die partial()-Funktion.

Code: Alles auswählen

Duration = benchmark(partial(insert_records, connection, number_of_records))
Ich habe versucht mich schlau zu machen. Wenn ich es richtig verstehe, dient diese Funktion als eine Art Vereinfachung der Funktionsschnittstellen. Zunächst zum Aufbau: Der partial()partial-Funktion werden entweder Schlüsselparameter oder Positionsparameter übergeben werden. Hier in diesem Falle werden Schlüsselparameter übergeben. Beim Aufruf der partial()-Funktion wird das Funktionsobjekt von insert_records als erster Parameter übergeben. Nun, connection und number_of_records werden hier als Schlüsselparameter übergeben. Auf diese Weise gibt die partial()-Funktion ein Funktionsobjekt zurück, welches insert_records()-Funktionsschnittstelle vereinfacht. Und das neue Funktionsobjekt wird der Benchmark()-Funktion als Parameter übergeben.

benchmark():
Anmerkung: Da ich kein Python 3 habe, konnte ich die perf_counter()-Funktion nicht benutzen - also bleibt es bei der datetime()-Funktion.

Code: Alles auswählen

def benchmark(function):
    start = perf_counter()
    function()
    end = perf_counter()
    return end - start
Benchmark erhält das neue Funktionsobjekt als Parameter. Und da ich als Entwickler weiß, dass der Parameter ein Funktionsobjekt ist, kann ich diesen auch direkt als Funktion aufrufen.

generate_names:

Code: Alles auswählen

def generate_names():
    for i in count():
        yield 'Name #{}'.format(i)
Hier hat mich das Schlüsselwort yield verwirrt. Auch das habe ich zuvor nicht angewendet. Nach meiner Recherche zufolge handelt es sich hierbei um ein Generator, des zwar auf dem ersten Blick wie eine normale Funktion aussieht, sich aber durch den yield unterscheidet. Denn eine normale Funktion hätte nur den return. Bei einem Generator werden __iter__() und next() beim Aufruf automatisch mit erzeugt - man muss sich darum keine Gedanken machen. Das heißt, beim Aufruf einer normalen Funktion werden Objekte wie Listen, Dictionaries etc. in einem Rutsch über return wiedergegeben. Der Generator kann Objekte Stückweise zurückgeben, denn der Generator "merkt" sich den lokalen Zustand.
Und jetzt taucht schon meine erste Frage auf. Wo ist in DasIchs optimierten Quelltext der Iterator? Denn auf vielen Seiten sieht das Beisiel wie folgt aus:

Code: Alles auswählen

def abc_generator():
    yield("a")
    yield("b")
    yield("c")

# There are two iterators: gen1 and gen2
gen1 = abc_generator()
gen2 = abc_generator()

letter = next(gen1)
print letter

letter = next(gen1)
print letter
insert_records:
Ich hänge hier an folgendem Ausdruck fest:

Code: Alles auswählen

*((name, ) for name in islice(generate_names(), number_of_records)
Es handelt sich hierbei um eine List Comprehensions oder? Hier verstehe ich leider nur Bahnhof, auch weil islice mit im Spiel ist. Also hier wäre ich wirklich dankbar, wenn ich eine leicht verständliche Erklärung bekäme.

Nun zwei Fragen zum Schluss. Aus welchem Grund wurde die auseigene input()-Funktion überdeckt? Hätte ein raw_input in einem try/except-Block in der main() nicht ausgereicht? Ich bin verwirrt. Und die letzt Frage ist: Welche Stelle im Code macht das Eintragen so dermaßen fix? Anders gefragt: Wer oder was ist dafür verantwortlich, dass die Einträge so dermaßen rasch vorgenommen werden? Denn die Optimierung ist wirklich unglaublich gut.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Sophus hat geschrieben: main():
Was mir hier sehr neu ist, ist die partial()-Funktion.[...]
Deine Erklärung ist im Prinzip hier richtig wenn auch kompliziert. Im wesentlichen kannst du mit partial eine Funktion erstellen, die die übergebene Funktion mit den an partial übergebenen Argumenten aufruft. Wenn man nicht alle Argumente an partial gegeben hat, kann man der neuen Funktion die fehlenden Argumente noch übergeben.
Benchmark erhält das neue Funktionsobjekt als Parameter. Und da ich als Entwickler weiß, dass der Parameter ein Funktionsobjekt ist, kann ich diesen auch direkt als Funktion aufrufen.
Ja.
generate_names:

Code: Alles auswählen

def generate_names():
    for i in count():
        yield 'Name #{}'.format(i)
Hier hat mich das Schlüsselwort yield verwirrt. Auch das habe ich zuvor nicht angewendet. Nach meiner Recherche zufolge handelt es sich hierbei um ein Generator,[...]
Ja.
Und jetzt taucht schon meine erste Frage auf. Wo ist in DasIchs optimierten Quelltext der Iterator? Denn auf vielen Seiten sieht das Beisiel wie folgt aus:[...]
Einen Iterator bekommt man wenn man generate_names() aufruft, was später dann auch passiert.
insert_records:
Ich hänge hier an folgendem Ausdruck fest:

Code: Alles auswählen

*((name, ) for name in islice(generate_names(), number_of_records)
Es handelt sich hierbei um eine List Comprehensions oder? Hier verstehe ich leider nur Bahnhof, auch weil islice mit im Spiel ist. Also hier wäre ich wirklich dankbar, wenn ich eine leicht verständliche Erklärung bekäme.
Es ist eine generator comprehension und keine list comprehension. Es gibt aber keinen großen Unterschied außer dass nur ein Iterator erzeugt wird und keine Liste. islice() ist nichts anderes als eine Funktion die slicing für alle iterables implementiert und funktioniert im Prinzip genauso wie die slicing Syntax die du z.B. bei Listen benutzen kannst.
Nun zwei Fragen zum Schluss. Aus welchem Grund wurde die auseigene input()-Funktion überdeckt? Hätte ein raw_input in einem try/except-Block in der main() nicht ausgereicht?
Es gibt eigentlichen keine sonderlich guten Grund input() zu überdecken, einige würden es auch für eine schlechte Idee halten und die Funktion anders nennen. Grundsätzlich verwende ich die Funktion aus dem gleichen Grund aus dem ich alle anderen Funktionen auch definiere. Ich möchte in main() vom User einen Integer haben der mir sagt wieviele Einträge ich machen soll. Wie ich vom User ein Integer bekomme ist ein anderes Problem dass mich an der Stelle nicht interessiert und mit dem ich mich an der Stelle nicht befassen möchte.

Und die letzt Frage ist: Welche Stelle im Code macht das Eintragen so dermaßen fix?
connection.execute wird nur einmal mit allen Werten aufgerufen. Das entspricht executemany in der DB API 2.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@DasIch: Danke für deine Erklärung. Jedoch bin ich beim Ausdruck des Generator Comprehensions stecken geblieben.

Code: Alles auswählen

*((name, ) for name in islice(generate_names(), number_of_records)
Znächst einmal erkenne ich die GC daran, dass hier mit runden Klammern "abgegrenzt" wird, während in der LC mit eckigen Klammern gearbeitet wird. So weit so gut. Jetzt versuche ich diese GC in Worte zu beschreiben - von innen nach außen. In der islice werden grundsätzlich zwei Argumente übergeben. Im ersten Argument wird hier die count()-Funktion eingesetzt. Diese Count()-Funktion beziehst du von der generate_names-Funktion, weil dort die Count()-Funktion steckt. Als zweites Argument übergibst du die Anzahl der Einträge. Somit ist es eine Art Stop-Operator. Das heißt "Tue etwas solange bis (number_of_records)" Im nächsten Schritt sind wir in der For-Schleife. Die einzelnen Datensätze werden über die For-Schleife iteriert und gleich in (name, ) gespeichert. Hier gleich die erste Frage: wieso das Komma und das Leerzeichen hinter name? Kommt da noch was? Und dann macht mich das Sternchen * ganz verrückt. Was mach das Sternchen da?

Mein Fazit: Der große Vorteil bei dem GC ist wohl, dass es nicht, wie LC das macht, eine komplette Liste-Instanz erstellt und dort alle Elemente speichert und somit den Speicherplatz im RAM vollstopft. Hier werden einzelne Elemente einzeln abgearbeitet und gleich danach aus dem Speicher entfernt, und ein neues Element ist dann auch im Anmarsch. Eine Art pue a pue -Arbeit.
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: 17753
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?
Antworten