wake-up-neo.net

Schnellere Abfrage von sqlite3 in go? Ich muss 1 Million Zeilen so schnell wie möglich verarbeiten

Wie liest man am schnellsten eine sqlite3-Tabelle in Golang?

package main

import (
    "fmt"
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "log"
    "time"
)

func main() {
    start := time.Now()

    db, err := sql.Open("sqlite3", "/Users/robertking/go/src/bitbucket.org/thematicanalysis/optimization_test/robs.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    rows, err := db.Query("select * from data")
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
    }
    err = rows.Err()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(time.Since(start))
}

Dies dauert 8 Sekunden in Go, da .Nextlangsam ist. In Python dauert eine fetchall nur 4 Sekunden! Ich schreibe in GO um, um Leistung zu gewinnen und Leistung nicht zu verlieren.

Hier ist der Python-Code, ich konnte kein Äquivalent zu fetchall in go finden:

import time

start = time.time()
import sqlite3
conn = sqlite3.connect('/Users/robertking/go/src/bitbucket.org/thematicanalysis/optimization_test/robs.db')
c = conn.cursor()
c.execute("SELECT * FROM data")
x = c.fetchall()
print time.time() - start

Bearbeiten: Kopfgeld hinzufügen. Ich lese die Daten in go, Python und C, hier sind die Ergebnisse. Will nicht C verwenden, bleibt aber bei Python, wenn GO nicht schneller ist .:

py: 2.45s
go: 2.13s (using github.com/mxk/go-sqlite/sqlite3 instead of github.com/mattn/go-sqlite3)
c:  0.32s

Ich fühle mich wie gehen sollte näher an der c-Seite der Sache sein? weiß jemand, wie es schneller geht? Kann man mit Readonly-Modus den Mutex vermeiden?

bearbeiten:

Es scheint, dass alle Implementierungen von sqlite3 langsam sind (zu viele Reflektionen und zu viele cgo-Aufrufe für Konvertierungen). Also muss ich einfach meine eigene Schnittstelle schreiben.

Hier ist das Schema:

CREATE TABLE mytable
(
  c0   REAL,
  c1   INTEGER,
  c15  TEXT,
  c16  TEXT,
  c17  TEXT,
  c18  TEXT,
  c19  TEXT,
  c47  TEXT,
  c74  REAL DEFAULT 0,
  c77  TEXT,
  c101 TEXT,
  c103 TEXT,
  c108 TEXT,
  c110 TEXT,
  c125 TEXT,
  c126 TEXT,
  c127 REAL DEFAULT 0,
  x    INTEGER
    PRIMARY KEY
);

und die Abfrage ist dynamisch, aber normalerweise ungefähr so:

SELECT c77,c77,c125,c126,c127,c74 from mytable

bearbeiten:

es sieht so aus, als würde ich die sqlite3-Implementierung untergliedern und einige Methoden erstellen, die sich auf die Leistung konzentrieren, 

dies ist ein Beispiel für Code, der viel schneller ist:.

package main


/*
 #cgo LDFLAGS: -l sqlite3

#include "sqlite3.h"
*/
import "C"

import (
    //"database/sql"
    "log"
    "reflect"
    "unsafe"
)

type Row struct {
    v77 string
    v125 string
    v126 string
    v127 float64
    v74 float64
}

// cStr returns a pointer to the first byte in s.
func cStr(s string) *C.char {
    h := (*reflect.StringHeader)(unsafe.Pointer(&s))
    return (*C.char)(unsafe.Pointer(h.Data))
}

func main() {
    getDataFromSqlite()
}

func getDataFromSqlite() {
    var db *C.sqlite3
    name := "../data_dbs/all_columns.db"
    rc := C.sqlite3_open_v2(cStr(name+"\x00"), &db, C.SQLITE_OPEN_READONLY, nil)

  var stmt *C.sqlite3_stmt;
  rc = C.sqlite3_prepare_v2(db, cStr("SELECT c77,c125,c126,c127,c74 from data\x00"), C.int(-1), &stmt, nil);
  rc = C.sqlite3_reset(stmt);

    var result C.double
    result = 0.0
    rc = C.sqlite3_step(stmt)
    for rc == C.SQLITE_ROW {
    C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 0))))
    C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 1))))
    C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 2))))
    C.sqlite3_column_double(stmt, 3)
    result += C.sqlite3_column_double(stmt, 4)
        rc = C.sqlite3_step(stmt)
  }
    log.Println(result)
}
23
robert king

Einführung

Meine Vermutung war, dass wir ein Problem damit haben, wie die Leistung hier gemessen wird. Deshalb schrieb ich ein kleines Go-Programm, um Datensätze zu generieren und in einer SQLite-Datenbank zu speichern, sowie eine Python- und Go-Implementierung einer kleinen Aufgabe für diese Datensätze .

Das entsprechende Repository finden Sie unter https://github.com/mwmahlberg/sqlite3perf

Das Datenmodell

Die erzeugten Datensätze bestehen aus 

Das Schema der Tabelle ist relativ einfach:

sqlite> .schema
CREATE TABLE bench (ID int PRIMARY KEY ASC, Rand TEXT, hash TEXT);

Zuerst habe ich 1,5 Millionen Datensätze generiert und anschließend die sqlite-Datenbank mit abgesaugt

$ ./sqlite3perf generate -r 1500000 -v

Als Nächstes rief ich die Go-Implementierung gegen diese 1,5-Millionen-Datensätze an. Sowohl die Go- als auch die Python-Implementierung führen im Wesentlichen die gleiche einfache Aufgabe aus:

  1. Lesen Sie alle Einträge aus der Datenbank.
  2. Dekodieren Sie für jede Zeile den Zufallswert aus Hex und erstellen Sie dann aus dem Ergebnis ein SHA256-Hex.
  3. Vergleichen Sie den generierten SHA256-Hex-String mit dem in der Datenbank gespeicherten String
  4. Wenn sie übereinstimmen, fahren Sie fort, sonst brechen Sie ab.

Annahmen

Meine Vermutung lautete ausdrücklich, dass Python eine Art Lazy Loading und/oder möglicherweise sogar die Ausführung der SQL-Abfrage ausgeführt hat.

Die Ergebnisse

Go Implementierung

$ ./sqlite3perf bench
2017/12/31 15:21:48 bench called
2017/12/31 15:21:48 Time after query: 4.824009ms
2017/12/31 15:21:48 Beginning loop
2017/12/31 15:21:48 Acessing the first result set 
    ID 0,
    Rand: 6a8a4ad02e5e872a,
    hash: 571f1053a7c2aaa56e5c076e69389deb4db46cc08f5518c66a4bc593e62b9aa4
took 548.32µs
2017/12/31 15:21:50 641,664 rows processed
2017/12/31 15:21:52 1,325,186 rows processed
2017/12/31 15:21:53 1,500,000 rows processed
2017/12/31 15:21:53 Finished loop after 4.519083493s
2017/12/31 15:21:53 Average 3.015µs per record, 4.523936078s overall

Notieren Sie sich die Werte für "Zeit nach Abfrage" (die Zeit, die der Abfragebefehl für die Rückkehr benötigte) und die Zeit, die der Zugriff auf die erste Ergebnismenge nach dem Starten der Iteration über die Ergebnismenge benötigte.

Python-Implementierung

$ python bench.py 
12/31/2017 15:25:41 Starting up
12/31/2017 15:25:41 Time after query: 1874µs
12/31/2017 15:25:41 Beginning loop
12/31/2017 15:25:44 Accessing first result set
    ID: 0
    Rand: 6a8a4ad02e5e872a
    hash: 571f1053a7c2aaa56e5c076e69389deb4db46cc08f5518c66a4bc593e62b9aa4
took 2.719312 s
12/31/2017 15:25:50 Finished loop after 9.147431s
12/31/2017 15:25:50 Average: 6.098µs per record, 0:00:09.149522 overall

Beachten Sie erneut den Wert für "Zeit nach Abfrage" und die Zeit, die für den Zugriff auf die erste Ergebnismenge benötigt wurde.

Zusammenfassung

Es dauerte eine Weile, bis die Go-Implementierung zurückkehrte, nachdem die SELECT-Abfrage gesendet wurde, während Python schien im Vergleich schnell zu sein schien. Von der Zeit bis zum tatsächlichen Zugriff auf die erste Ergebnismenge hat sich jedoch herausgestellt, dass die Go-Implementierung für den Zugriff auf die erste Ergebnismenge (5.372329ms vs. 2719.312ms) mehr als 500-mal schneller ist und für die Aufgabe etwa doppelt so schnell ist zur Hand wie die Python-Implementierung.

Anmerkungen

  • Um die Annahme zu belegen, dass Python die Ergebnismenge tatsächlich träge lädt, musste auf jede Zeile und jede Spalte zugegriffen werden, um sicherzustellen, dass Python gezwungen ist, den Wert tatsächlich aus der Datenbank zu lesen.
  • Ich habe mich für eine Hash-Aufgabe entschieden, da die Implementierung von SHA256 vermutlich in beiden Sprachen stark optimiert ist.

Fazit

Python scheint ein faules Laden von Ergebnismengen durchzuführen und führt möglicherweise sogar keine Abfrage aus, es sei denn, auf die entsprechende Ergebnismenge wird tatsächlich zugegriffen. In diesem simulierten Szenario übertrifft der SQLite-Treiber von mattn für Go die Leistung von Python um etwa 100% und Größenordnungen, je nachdem, was Sie tun möchten.

Edit: Um eine schnelle Verarbeitung zu erreichen, implementieren Sie Ihre Aufgabe in Go. Das Senden der eigentlichen Abfrage dauert zwar länger, der Zugriff auf die einzelnen Zeilen der Ergebnismenge ist jedoch wesentlich schneller. Ich würde vorschlagen, mit einer kleinen Teilmenge Ihrer Daten zu beginnen, etwa 50.000 Datensätze. Verwenden Sie dann zur weiteren Verbesserung Ihres Codes profiling , um Ihre Engpässe zu identifizieren. Abhängig davon, was Sie während der Verarbeitung tun möchten, können beispielsweise Pipelines helfen. Die Verbesserung der Verarbeitungsgeschwindigkeit der vorliegenden Aufgabe ist jedoch ohne tatsächlichen Code oder eine ausführliche Beschreibung schwer zu sagen.

19

Abtastwerte aus den abgerufenen Zeilen Beispiel Leseschritt 10 .
Da Query() & QueryRow() aus der Datenbankabfrage jeweils einen Zeiger auf Zeilen und Zeiger auf Row zurückgibt, können wir die Scan () - Funktionen in Rows- und Row-Struktur verwenden, um Zugriff auf die Werte in der Zeilenstruktur zu erhalten.

for rows.Next() {

 var empID sql.NullInt64
 var empName sql.NullString
 var empAge sql.NullInt64
 var empPersonId sql.NullInt64

 if err := rows.Scan(&empID, &empName, &empAge, 
                           &empPersonId); err != nil {
          log.Fatal(err)
 }

 fmt.Printf("ID %d with personID:%d & name %s is age %d\n",       
                   empID.Int64, empPersonId.Int64, empName.String, empAge.Int64)
}

Wir haben auch die Funktion Scan () aus der Zeilenstruktur verwendet. Scan () ist die einzige in der Zeilenstruktur deklarierte Methode.

func (r *Row) Scan(dest ...interface{}) error
1
user5377037