Komunikace se sloupcovými databázemi z jazyka Go: Parquet soubory (dokončení)

19. 11. 2020
Doba čtení: 35 minut

Sdílet

 Autor: Go Lang
Dnes dokončíme popis přímé manipulace s Parquet soubory v jazyce Go s využitím knihovny parquet-go. Zaměříme se především na rychlost přístupu, protože právě vyšší rychlost čtení dat je hlavní výhodou sloupcových databází.

Obsah

1. Komunikace se sloupcovými databázemi z jazyka Go: Parquet soubory (dokončení)

2. Rychlost zápisu vs. rychlost čtení z Parquet souborů

3. Čtení ze sloupcové databáze po záznamech je pomalé!

4. Čtení dat po blocích

5. Vliv velikosti bloku na rychlost čtení dat

6. Vyhodnocení výsledků

7. Konstantní počet gorutin pro čtení a jejich vliv na rychlost zpracování Parquet souborů

8. Vyhodnocení výsledků – použití jedné resp. 100 gorutin při čtení

9. Odvození počtu gorutin od velikosti bloku

10. Vyhodnocení výsledků – odvození počtu gorutin od velikosti bloku

11. Čtení hodnot z vybraného sloupce

12. Použití indexu sloupce při čtení

13. Zjištění počtu aktivních a neaktivních uživatelů

14. Výsledky – čtení a zpracování dat z jednoho sloupce

15. Specifikace čteného sloupce jeho jménem (cestou)

16. Rychlost přečtení všech údajů z jediného sloupce

17. Výsledky měření rychlosti

18. Pomocné skripty pro tvorbu grafů

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Komunikace se sloupcovými databázemi z jazyka Go: Parquet soubory (dokončení)

V dnešním článku, který svým zaměřením přímo navazuje na článek předchozí, dokončíme popis práce s Parquet soubory v programovacím jazyku Go. Zaměříme se na dvě oblasti. Jednou z nich je rychlost zápisu a čtení z Parquet souborů, protože k tomuto formátu se většinou uchylujeme ve chvíli, kdy je zapotřebí zajistit velmi rychlý přístup k jednotlivým sloupcům (a pouhé tvrzení „sloupcové databáze jsou rychlé“ pochopitelně v praxi neobstojí a musí se dokázat, za jakých předpokladů platí). A právě čtení jednotlivých sloupců je druhým důležitým tématem, kterým se dnes budeme zabývat.

Poznámka: jen pro připomenutí – zajímat nás bude přímá práce s Parquet soubory; nebudeme se tedy zabývat například dotazovacím jazykem apod. To je téma na samostatný článek, který navíc nebude přímo souviset s programovacím jazykem Go.
Poznámka2: ve všech benchmarcích se – pokud není stanoveno jinak – ukládá či naopak načítá jeden milion záznamů, popř. jeden milion hodnot z vybraného sloupce. Všechny výpočty časů jsou z tohoto důvodu poměrně přímočaré.

2. Rychlost zápisu vs. rychlost čtení z Parquet souborů

Na konci předchozího článku jsme si uvedli dva příklady, které slouží pro jednoduché zjištění, jak dlouho trvá vytvoření a naplnění nového Parquet souboru daty a jak rychle je naopak možné tato data přečíst. Pro vytváření souboru, resp. jednotlivých záznamů, použijeme knihovnu faker, která nám vygeneruje pseudonáhodná data:

package main
 
import (
        "log"
        "math/rand"
        "os"
        "time"
 
        "github.com/bxcodec/faker/v3"
        "github.com/xitongsys/parquet-go/parquet"
        "github.com/xitongsys/parquet-go/writer"
)
 
const defaultOutputFile = "flat.parquet"
 
// Record represents one record stored in Parquet file
type Record struct {
        ID      uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"`
        Name    string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"`
        Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"`
        Email   string `parquet:"name=email, type=UTF8, encoding=PLAIN"`
        Active  bool   `parquet:"name=active, type=BOOLEAN"`
        Color   string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"`
}
 
func generateColor() string {
        var colors []string = []string{
                "black",
                "blue",
                "red",
                "magenta",
                "green",
                "cyan",
                "yellow",
                "white",
        }
        return colors[rand.Int()%len(colors)]
}
 
func writeRecords(pw *writer.ParquetWriter, n int) {
        // create report structure to be stored in Parquet file
        record := Record{}
 
        for i := 0; i < n; i++ {
                record.ID = uint64(i)
                record.Name = faker.FirstName()
                record.Surname = faker.LastName()
                record.Email = faker.Email()
                record.Active = i%2 == 0
                record.Color = generateColor()
 
                // write the record structure into Parquet file
                err := pw.Write(record)
                if err != nil {
                        log.Println("Write into Parquet error", err)
                }
        }
}
 
// stopWrite function stop writing into Parquet file
func stopWrite(pw *writer.ParquetWriter) {
        err := pw.WriteStop()
 
        // most write errors are caught at this time
        if err != nil {
                log.Println("WriteStop error", err)
        }
}
 
func createAndWriteIntoParquetFile(filename string, records int, compression parquet.CompressionCodec) {
        t1 := time.Now()
 
        w, err := os.Create(filename)
        if err != nil {
                log.Println("Can't create local file", err)
                return
        }
 
        defer w.Close()
 
        // initialize Parquet file writer
        pw, err := writer.NewParquetWriterFromWriter(w, new(Record), 1)
        if err != nil {
                log.Println("Can't create parquet writer", err)
                return
        }
 
        pw.RowGroupSize = 128 * 1024 * 1024 //128M
        pw.CompressionType = compression
 
        defer stopWrite(pw)
 
        writeRecords(pw, records)
 
        log.Println("Write Finished")
 
        // compute and print duration
        t2 := time.Now()
        since := time.Since(t1)
        log.Println("Start time: ", t1)
        log.Println("End time:   ", t2)
        log.Println("Duration:   ", since)
}
 
func main() {
        createAndWriteIntoParquetFile("1000000records_compression_none.parquet", 1000000, parquet.CompressionCodec_UNCOMPRESSED)
        createAndWriteIntoParquetFile("1000000records_compression_snappy.parquet", 1000000, parquet.CompressionCodec_SNAPPY)
        createAndWriteIntoParquetFile("1000000records_compression_gzip.parquet", 1000000, parquet.CompressionCodec_GZIP)
}

Časy trvání zápisu do ramdisku s využitím obou komprimačních algoritmů (první zápis nepoužívá žádný algoritmus):

2020/11/14 16:22:21 Write Finished
2020/11/14 16:22:21 Start time:  2020-11-14 16:22:18.382414375 +0100 CET m=+0.001018949
2020/11/14 16:22:21 End time:    2020-11-14 16:22:21.496799932 +0100 CET m=+3.115404464
2020/11/14 16:22:21 Duration:    3.114385625s
 
2020/11/14 16:22:24 Write Finished
2020/11/14 16:22:24 Start time:  2020-11-14 16:22:21.52651968 +0100 CET m=+3.145124247
2020/11/14 16:22:24 End time:    2020-11-14 16:22:24.81071525 +0100 CET m=+6.429319812
2020/11/14 16:22:24 Duration:    3.284195685s
 
2020/11/14 16:22:28 Write Finished
2020/11/14 16:22:28 Start time:  2020-11-14 16:22:24.835851362 +0100 CET m=+6.454455962
2020/11/14 16:22:28 End time:    2020-11-14 16:22:28.88592985 +0100 CET m=+10.504534394
2020/11/14 16:22:28 Duration:    4.050078532s

Taktéž jsme si uvedli příklad sloužící pro otestování rychlosti čtení po jednotlivých záznamech. Jedná se o nejpomalejší možný způsob čtení, protože zde vůbec nevyužijeme výhod poskytovaných sloupcovou databází:

// This tool is able to read all records stored in selected Parquet file.
// Currently, only records with the structure `Record` is read correctly. Name
// of input Parquet file needs to be selected from command line.
package main
 
import (
        "log"
        "time"
 
        "github.com/xitongsys/parquet-go-source/local"
        "github.com/xitongsys/parquet-go/reader"
        "github.com/xitongsys/parquet-go/source"
)
 
// Record represents one record stored in Parquet file
type Record struct {
        ID      uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"`
        Name    string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"`
        Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"`
        Email   string `parquet:"name=email, type=UTF8, encoding=PLAIN"`
        Active  bool   `parquet:"name=active, type=BOOLEAN"`
        Color   string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"`
}
 
// closeReader tries to close the given Parquet file reader
func closeReader(reader source.ParquetFile) {
        err := reader.Close()
        if err != nil {
                log.Println("close reader:", err)
        }
}
 
func readParquetFile(fileName string) {
        t1 := time.Now()
 
        const parallelNumber = 1
 
        // construct the file reader and try to open the Parquet file for
        // reading
        fileReader, err := local.NewLocalFileReader(fileName)
 
        if err != nil {
                log.Fatal("Can't open file", err)
                return
        }
 
        // fileReader needs to be closed properly
        defer closeReader(fileReader)
 
        // initialize Parquet file reader
        parquetReader, err := reader.NewParquetReader(fileReader, new(Record),
                parallelNumber)
 
        if err != nil {
                log.Fatal("Can't create parquet reader", err)
                return
        }
 
        // parquetReader needs to be stopped
        defer parquetReader.ReadStop()
 
        readRecords(parquetReader)
 
        // compute and print duration
        t2 := time.Now()
        since := time.Since(t1)
        log.Println("Start time: ", t1)
        log.Println("End time:   ", t2)
        log.Println("Duration:   ", since)
}
 
func readRecords(parquetReader *reader.ParquetReader) {
        recordCount := int(parquetReader.GetNumRows())
        log.Println("Records to read", recordCount)
 
        record := make([]Record, 1)
        records := 0
 
        // try to read and display all records
        for i := 0; i < recordCount; i++ {
                // try to read record
                err := parquetReader.Read(&record)
                if err != nil {
                        log.Println("Read error", err)
                        continue
                } else {
                        records++
                }
        }
        log.Println("Read", records, "records")
}
 
func main() {
        readParquetFile("1000000records_compression_none.parquet")
        readParquetFile("1000000records_compression_snappy.parquet")
        readParquetFile("1000000records_compression_gzip.parquet")
}

Podívejme se nyní na dosažené výsledky. Rychlost čtení (po jednotlivých záznamech) je mnohem pomalejší, než samotný zápis do sloupcové databáze:

2020/11/14 16:46:53 Records to read 1000000
2020/11/14 16:47:17 Read 1000000 records
2020/11/14 16:47:17 Start time:  2020-11-14 16:46:53.80851109 +0100 CET m=+0.000895204
2020/11/14 16:47:17 End time:    2020-11-14 16:47:17.695641899 +0100 CET m=+23.888025988
2020/11/14 16:47:17 Duration:    23.887130935s
2020/11/14 16:47:17 Records to read 1000000
2020/11/14 16:47:41 Read 1000000 records
2020/11/14 16:47:41 Start time:  2020-11-14 16:47:17.695696876 +0100 CET m=+23.888080959
2020/11/14 16:47:41 End time:    2020-11-14 16:47:41.460809934 +0100 CET m=+47.653194032
2020/11/14 16:47:41 Duration:    23.765113146s
2020/11/14 16:47:41 Records to read 1000000
2020/11/14 16:48:05 Read 1000000 records
2020/11/14 16:48:05 Start time:  2020-11-14 16:47:41.460860147 +0100 CET m=+47.653244228
2020/11/14 16:48:05 End time:    2020-11-14 16:48:05.50961075 +0100 CET m=+71.701994837
2020/11/14 16:48:05 Duration:    24.048750743s

3. Čtení ze sloupcové databáze po záznamech je pomalé!

Výsledky získané z obou předchozích příkladů a shrnuté do jediné tabulky ukazují, že čtení po jednotlivých záznamech je skutečně pomalé:

# Komprimace Zápis Čtení
1 None 3.15 23.88s
2 Snappy 3.30s 23.76s
3 GZIP 4.07s 24.04s
Poznámka: tento přístup, tedy čtení po záznamech, by se pravděpodobně nikdy neměl používat v praxi, protože opravu znamená, že se sloupová databáze využívá značně neefektivním způsobem.

4. Čtení dat po blocích

Pokud z nějakého důvodu potřebujete skutečně zpracovávat data po záznamech a nikoli po sloupcích, je obecně rychlejší použít čtení po celých blocích. Je to vlastně velmi jednoduché. Postačuje tento kus kódu…

recordCount := int(parquetReader.GetNumRows())
 
record := make([]Record, 1)
 
// try to read and display all records
for i := 0; i < recordCount; i++ {
        // try to read record
        err := parquetReader.Read(&record)
        if err != nil {
                log.Println("Read error", err)
                continue
        } else {
                // zde se může se záznamem pracovat
        }
}

…nahradit za čtení po větších blocích, jejichž délku si můžete určit (až pochopitelně na poslední blok, který bude obecně menší):

recordCount := int(parquetReader.GetNumRows())
 
records := make([]Record, blockSize)
readRecords := 0
 
// try to read and display all records
for readRecords < recordCount {
        // try to read record
        err := parquetReader.Read(&records)
        if err != nil {
                log.Println("Read error", err)
                continue
        } else {
                readRecords += len(records)
                // zde se může se záznamy v řezu pracovat
        }
}
// log.Println("Read", readRecords, "records")

5. Vliv velikosti bloku na rychlost čtení dat

Nyní tedy umíme pracovat s daty po větších blocích. Jaký je však vliv velikosti bloku na rychlost čtení? To je obecně velmi důležitá informace, protože nutnost alokovat velké bloky může mít negativní vliv na paměťové nároky a/nebo i na rychlost celé aplikace. Můžeme se pokusit provést malé měření a velikost bloku postupně zvětšovat od jednoho záznamu až po 1000 (resp. přesněji maxBlockSize) záznamů. Po každém zvětšení bloku opět přečteme všechny záznamy a zaznamenáme celkový čas:

// This tool is able to read all records stored in selected Parquet file.
// Currently, only records with the structure `Record` is read correctly. Name
// of input Parquet file needs to be selected from command line.
package main
 
import (
        "encoding/csv"
        "log"
        "os"
        "strconv"
        "time"
 
        "github.com/xitongsys/parquet-go-source/local"
        "github.com/xitongsys/parquet-go/reader"
        "github.com/xitongsys/parquet-go/source"
)
 
// maximum block size for reading Parquet files by blocks
const maxBlockSize = 1000
 
// Record represents one record stored in Parquet file
type Record struct {
        ID      uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"`
        Name    string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"`
        Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"`
        Email   string `parquet:"name=email, type=UTF8, encoding=PLAIN"`
        Active  bool   `parquet:"name=active, type=BOOLEAN"`
        Color   string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"`
}
 
// closeReader tries to close the given Parquet file reader
func closeReader(reader source.ParquetFile) {
        err := reader.Close()
        if err != nil {
                log.Println("close reader:", err)
        }
}
 
func readParquetFile(fileName string, blockSize int) {
        // construct the file reader and try to open the Parquet file for
        // reading
        fileReader, err := local.NewLocalFileReader(fileName)
 
        if err != nil {
                log.Fatal("Can't open file", err)
                return
        }
 
        // fileReader needs to be closed properly
        defer closeReader(fileReader)
 
        // initialize Parquet file reader
        parquetReader, err := reader.NewParquetReader(fileReader, new(Record), 1)
 
        if err != nil {
                log.Fatal("Can't create parquet reader", err)
                return
        }
 
        // parquetReader needs to be stopped
        defer parquetReader.ReadStop()
 
        readRecords(parquetReader, blockSize)
}
 
func readRecords(parquetReader *reader.ParquetReader, blockSize int) {
        recordCount := int(parquetReader.GetNumRows())
        // log.Println("Records to read", recordCount)
 
        records := make([]Record, blockSize)
        readRecords := 0
 
        // try to read and display all records
        for readRecords < recordCount {
                // try to read record
                err := parquetReader.Read(&records)
                if err != nil {
                        log.Println("Read error", err)
                        continue
                } else {
                        readRecords += len(records)
                }
        }
        // log.Println("Read", readRecords, "records")
}
 
func main() {
        // create and open new CSV file
        csvFile, err := os.Create("durations.csv")
        if err != nil {
                log.Fatal("Create CSV file", err)
        }
 
        defer csvFile.Close()
 
        // initialize CSV writer
        csvWriter := csv.NewWriter(csvFile)
        defer csvWriter.Flush()
 
        csvWriter.Write([]string{"Block size", "Time to read"})
 
        for blockSize := 1; blockSize <= maxBlockSize; blockSize++ {
                t1 := time.Now()
 
                readParquetFile("1000000records_compression_none.parquet", blockSize)
 
                // compute and print duration
                since := time.Since(t1)
                log.Printf("Block size: %d  Duration: %d\n", blockSize, since)
 
                // write duration into CSV file
                csvWriter.Write([]string{strconv.Itoa(blockSize), strconv.Itoa(int(since))})
        }
}

6. Vyhodnocení výsledků

Z grafu, který je možné ze získaných dat vykreslit, je patrné, že čím větší je velikost bloku, tím kratší je celková doba nutná pro načtení všech záznamů. Na konci grafu jsou vidět „zákmity“ způsobené činností automatického správce paměti:

Obrázek 1: Vliv velikosti bloku (počet záznamů čtených jedinou operací) na rychlost načtení.

7. Konstantní počet gorutin pro čtení a jejich vliv na rychlost zpracování Parquet souborů

Při inicializaci objektu, který čtení z Parquet souborů realizuje, je možné specifikovat počet gorutin, v nichž je prováděno vlastní čtení. Prozatím jsme počet gorutin měli nastaven na hodnotu 1:

// initialize Parquet file reader
parquetReader, err := reader.NewParquetReader(fileReader, new(Record), 1)

Tento počet je však možné měnit a celkový počet gorutin bude mít vliv na rychlost čtení. Zda se jedná o záporný či spíše kladný vliv, se pokusíme zjistit v dalším příkladu, který těchto gorutin bude vyžadovat přesně 100 (konstanta readers), což je mimochodem mnohem více, než počet procesorových jader (osm fyzických jader tvářících se jako šestnáct jader logických):

// This tool is able to read all records stored in selected Parquet file.
// Currently, only records with the structure `Record` is read correctly. Name
// of input Parquet file needs to be selected from command line.
package main
 
import (
        "encoding/csv"
        "log"
        "os"
        "strconv"
        "time"
 
        "github.com/xitongsys/parquet-go-source/local"
        "github.com/xitongsys/parquet-go/reader"
        "github.com/xitongsys/parquet-go/source"
)
 
// maximum block size for reading Parquet files by blocks
const maxBlockSize = 1000
 
const readers = 100
 
// Record represents one record stored in Parquet file
type Record struct {
        ID      uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"`
        Name    string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"`
        Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"`
        Email   string `parquet:"name=email, type=UTF8, encoding=PLAIN"`
        Active  bool   `parquet:"name=active, type=BOOLEAN"`
        Color   string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"`
}
 
// closeReader tries to close the given Parquet file reader
func closeReader(reader source.ParquetFile) {
        err := reader.Close()
        if err != nil {
                log.Println("close reader:", err)
        }
}
 
func readParquetFile(fileName string, blockSize int) {
        // construct the file reader and try to open the Parquet file for
        // reading
        fileReader, err := local.NewLocalFileReader(fileName)
 
        if err != nil {
                log.Fatal("Can't open file", err)
                return
        }
 
        // fileReader needs to be closed properly
        defer closeReader(fileReader)
 
        // initialize Parquet file reader
        parquetReader, err := reader.NewParquetReader(fileReader, new(Record), readers)
 
        if err != nil {
                log.Fatal("Can't create parquet reader", err)
                return
        }
 
        // parquetReader needs to be stopped
        defer parquetReader.ReadStop()
 
        readRecords(parquetReader, blockSize)
}
 
func readRecords(parquetReader *reader.ParquetReader, blockSize int) {
        recordCount := int(parquetReader.GetNumRows())
        // log.Println("Records to read", recordCount)
 
        records := make([]Record, blockSize)
        readRecords := 0
 
        // try to read and display all records
        for readRecords < recordCount {
                // try to read record
                err := parquetReader.Read(&records)
                if err != nil {
                        log.Println("Read error", err)
                        continue
                } else {
                        readRecords += len(records)
                }
        }
        // log.Println("Read", readRecords, "records")
}
 
func main() {
        // create and open new CSV file
        csvFile, err := os.Create("durations.csv")
        if err != nil {
                log.Fatal("Create CSV file", err)
        }
 
        defer csvFile.Close()
 
        // initialize CSV writer
        csvWriter := csv.NewWriter(csvFile)
        defer csvWriter.Flush()
 
        csvWriter.Write([]string{"Block size", "Time to read"})
 
        for blockSize := 1; blockSize <= maxBlockSize; blockSize++ {
                t1 := time.Now()
 
                readParquetFile("1000000records_compression_none.parquet", blockSize)
 
                // compute and print duration
                since := time.Since(t1)
                log.Printf("Block size: %d  Duration: %d\n", blockSize, since)
 
                // write duration into CSV file
                csvWriter.Write([]string{strconv.Itoa(blockSize), strconv.Itoa(int(since))})
        }
}

8. Vyhodnocení výsledků – použití jedné resp. 100 gorutin při čtení

Časy běhu závislé na velikosti bloku budou v tomto případě odlišné:

Obrázek 2: Vliv velikosti bloku (počet záznamů čtených jedinou operací) na rychlost načtení při použití 100 gorutin.

Zajímavější je však porovnání s předchozím měřením s jedinou gorutinou:

Obrázek 3: Vliv počtu gorutin a současně i velikosti bloku na rychlost čtení.

Z grafu je patrné, že pokud je velikost bloku větší než počet gorutin, je výhodnější druhá možnost. Ovšem problém nastává při čtení po kratších blocích, kdy větší počet gorutin paradoxně vede ke zpomalení čtení (a to dokonce o celý řád!). Měli bychom tudíž přijít s lepším řešením a nějakým způsobem svázat počet gorutin s velikostí bloku.

9. Odvození počtu gorutin od velikosti bloku

V některých materiálech o knihovně určené pro čtení Parquet souborů se setkáme s tvrzením, že velikost bloku by měla odpovídat počtu gorutin, v nichž běží načítací rutina. Zda je toto tvrzení pravdivé, popř. pro jaké velikosti bloků je pravdivé, si ověříme v dalším demonstračním příkladu:

// This tool is able to read all records stored in selected Parquet file.
// Currently, only records with the structure `Record` is read correctly. Name
// of input Parquet file needs to be selected from command line.
package main
 
import (
        "encoding/csv"
        "log"
        "os"
        "strconv"
        "time"
 
        "github.com/xitongsys/parquet-go-source/local"
        "github.com/xitongsys/parquet-go/reader"
        "github.com/xitongsys/parquet-go/source"
)
 
// maximum block size for reading Parquet files by blocks
const maxBlockSize = 1000
 
// Record represents one record stored in Parquet file
type Record struct {
        ID      uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"`
        Name    string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"`
        Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"`
        Email   string `parquet:"name=email, type=UTF8, encoding=PLAIN"`
        Active  bool   `parquet:"name=active, type=BOOLEAN"`
        Color   string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"`
}
 
// closeReader tries to close the given Parquet file reader
func closeReader(reader source.ParquetFile) {
        err := reader.Close()
        if err != nil {
                log.Println("close reader:", err)
        }
}
 
func readParquetFile(fileName string, blockSize int) {
        // construct the file reader and try to open the Parquet file for
        // reading
        fileReader, err := local.NewLocalFileReader(fileName)
 
        if err != nil {
                log.Fatal("Can't open file", err)
                return
        }
 
        // fileReader needs to be closed properly
        defer closeReader(fileReader)
 
        // initialize Parquet file reader
        parquetReader, err := reader.NewParquetReader(fileReader, new(Record), int64(blockSize))
 
        if err != nil {
                log.Fatal("Can't create parquet reader", err)
                return
        }
 
        // parquetReader needs to be stopped
        defer parquetReader.ReadStop()
 
        readRecords(parquetReader, blockSize)
}
 
func readRecords(parquetReader *reader.ParquetReader, blockSize int) {
        recordCount := int(parquetReader.GetNumRows())
        // log.Println("Records to read", recordCount)
 
        records := make([]Record, blockSize)
        readRecords := 0
 
        // try to read and display all records
        for readRecords < recordCount {
                // try to read record
                err := parquetReader.Read(&records)
                if err != nil {
                        log.Println("Read error", err)
                        continue
                } else {
                        readRecords += len(records)
                }
        }
        // log.Println("Read", readRecords, "records")
}
 
func main() {
        // create and open new CSV file
        csvFile, err := os.Create("durations.csv")
        if err != nil {
                log.Fatal("Create CSV file", err)
        }
 
        defer csvFile.Close()
 
        // initialize CSV writer
        csvWriter := csv.NewWriter(csvFile)
        defer csvWriter.Flush()
 
        csvWriter.Write([]string{"Block size", "Time to read"})
 
        for blockSize := 1; blockSize <= maxBlockSize; blockSize++ {
                t1 := time.Now()
 
                readParquetFile("1000000records_compression_none.parquet", blockSize)
 
                // compute and print duration
                since := time.Since(t1)
                log.Printf("Block size: %d  Duration: %d\n", blockSize, since)
 
                // write duration into CSV file
                csvWriter.Write([]string{strconv.Itoa(blockSize), strconv.Itoa(int(since))})
        }
}

10. Vyhodnocení výsledků – odvození počtu gorutin od velikosti bloku

Nejprve se podívejme na vliv doby načtení 1000000 záznamů v blocích o velikosti od jednoho záznamu až po tisíc záznamů. Počet gorutin přesně odpovídá velikosti bloku:

Obrázek 4: Čtení po blocích rozdílné velikosti s rozdílným počtem gorutin.

Vidíme, že doporučení, aby se počet gorutin odvozoval od velikosti bloku, není zcela dobré. Ještě více je to patrné z následujícího grafu:

Obrázek 5: Čtení po blocích rozdílné velikosti s rozdílným počtem gorutin. Porovnání se čtením v jediné gorutině.

Doporučení by tedy mělo znít spíše takto: počet gorutin by měl odpovídat počtu logických procesorových jader. Pouze v případě, že je zapotřebí číst po jednotlivých záznamech, použijte jedinou gorutinu.

11. Čtení hodnot z vybraného sloupce

Sloupcové databáze jsou optimalizovány na to, aby se data načítala a zpracovávala po jednotlivých sloupcích. Parquet soubory nejsou výjimkou, takže i v knihovně parquet-go nalezneme dvě metody určené pro čtení hodnot z vybraného sloupce:

# Funkce Stručný popis
1 ReadColumnByIndex čtení ze sloupce vybraného pomocí indexu
2 ReadColumnByPath čtení ze sloupce vybraného cestou
Poznámka: nesmíme zapomenout na to, že se liší i objekt určený pro čtení. Tento objekt se získá konstruktorem NewParquetColumnReader a nikoli NewParquetReader:
fileReader, err := local.NewLocalFileReader(fileName)
 
// fileReader needs to be closed properly
defer closeReader(fileReader)
 
// initialize Parquet file reader
parquetColumnReader, err := reader.NewParquetColumnReader(fileReader, parallelNumber)

12. Použití indexu sloupce při čtení

Pro načtení jediné hodnoty ze sloupce s indexem columnIndex se použije metoda ReadColumnByIndex, které se předá index sloupce (čísluje se od nuly) a počet načítaných hodnot:

data, _, _, err := parquetReader.ReadColumnByIndex(int64(columnIndex), 1)
if err != nil {
        log.Println("Read error", err)
        continue
}

Typ vrácené hodnoty je řez (slice) hodnot implementujících prázdné rozhraní – jinými slovy se jedná o kolekci libovolných hodnot. O přetypování se musí postarat samotný program, a to například následovně:

if data[0].(bool) {
        activeCount++
} else {
        inactiveCount++
}
Poznámka: ve skutečnosti se z metody ReadColumnByIndex vrací čtyři hodnoty, ovšem pro nás je relevantní jen hodnota první a poslední – v poslední hodnotě se podle úzu vrací chyba.

13. Zjištění počtu aktivních a neaktivních uživatelů

V dalším demonstračním příkladu se zjišťuje počet aktivních a neaktivních uživatelů. Co to znamená? Z databáze se strukturou:

ID      uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"`
Name    string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"`
Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"`
Email   string `parquet:"name=email, type=UTF8, encoding=PLAIN"`
Active  bool   `parquet:"name=active, type=BOOLEAN"`
Color   string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"`

budeme zpracovávat pouze sloupec Active, jehož index je roven čtyřem:

const activeColumnIndex = 4

Příklad, který načte a zpracuje údaje z tohoto sloupce, by mohl vypadat následovně:

// This tool is able to read all records stored in selected Parquet file.
// Currently, only records with the structure `Record` is read correctly. Name
// of input Parquet file needs to be selected from command line.
package main
 
import (
        "log"
        "time"
 
        "github.com/xitongsys/parquet-go-source/local"
        "github.com/xitongsys/parquet-go/reader"
        "github.com/xitongsys/parquet-go/source"
)
 
// Record represents one record stored in Parquet file
type Record struct {
        ID      uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"`
        Name    string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"`
        Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"`
        Email   string `parquet:"name=email, type=UTF8, encoding=PLAIN"`
        Active  bool   `parquet:"name=active, type=BOOLEAN"`
        Color   string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"`
}
 
const activeColumnIndex = 4
 
// closeReader tries to close the given Parquet file reader
func closeReader(reader source.ParquetFile) {
        err := reader.Close()
        if err != nil {
                log.Println("close reader:", err)
        }
}
 
func readParquetFile(fileName string) {
        t1 := time.Now()
 
        const parallelNumber = 1
 
        // construct the file reader and try to open the Parquet file for
        // reading
        fileReader, err := local.NewLocalFileReader(fileName)
 
        if err != nil {
                log.Fatal("Can't open file", err)
                return
        }
 
        // fileReader needs to be closed properly
        defer closeReader(fileReader)
 
        // initialize Parquet file reader
        parquetColumnReader, err := reader.NewParquetColumnReader(fileReader, parallelNumber)
 
        if err != nil {
                log.Fatal("Can't create parquet column reader", err)
                return
        }
 
        // parquetReader needs to be stopped
        defer parquetColumnReader.ReadStop()
 
        readColumnData(parquetColumnReader, activeColumnIndex)
 
        // compute and print duration
        t2 := time.Now()
        since := time.Since(t1)
        log.Println("Start time: ", t1)
        log.Println("End time:   ", t2)
        log.Println("Duration:   ", since)
}
 
func readColumnData(parquetReader *reader.ParquetReader, columnIndex int) {
        valuesCount := int(parquetReader.GetNumRows())
        log.Println("Values to read", valuesCount)
 
        activeCount := 0
        inactiveCount := 0
 
        values := 0
 
        // try to read and display all records
        for i := 0; i < valuesCount; i++ {
                // try to read value
                data, _, _, err := parquetReader.ReadColumnByIndex(int64(columnIndex), 1)
                if err != nil {
                        log.Println("Read error", err)
                        continue
                } else {
                        values++
                }
                if data[0].(bool) {
                        activeCount++
                } else {
                        inactiveCount++
                }
        }
        log.Println("Read", values, "values", "active", activeCount, "inactive", inactiveCount)
}
 
func main() {
        readParquetFile("1000000records_compression_none.parquet")
        readParquetFile("1000000records_compression_snappy.parquet")
        readParquetFile("1000000records_compression_gzip.parquet")
}

14. Výsledky – čtení a zpracování dat z jednoho sloupce

Nezávisle na tom, který soubor (každý používá jiný komprimační algoritmus) je zpracováván, by se mělo vypočítat 500000 aktivních a 500000 neaktivních uživatelů, protože těmito hodnotami byla databáze naplněna:

2020/11/17 19:18:08 Values to read 1000000
2020/11/17 19:18:08 Read 1000000 values active 500000 inactive 500000
2020/11/17 19:18:08 Start time:  2020-11-17 19:18:08.01666144 +0100 CET m=+0.000954929
2020/11/17 19:18:08 End time:    2020-11-17 19:18:08.598879555 +0100 CET m=+0.583173023
2020/11/17 19:18:08 Duration:    582.218211ms
 
2020/11/17 19:18:08 Values to read 1000000
2020/11/17 19:18:09 Read 1000000 values active 500000 inactive 500000
2020/11/17 19:18:09 Start time:  2020-11-17 19:18:08.598904304 +0100 CET m=+0.583197768
2020/11/17 19:18:09 End time:    2020-11-17 19:18:09.145461682 +0100 CET m=+1.129755154
2020/11/17 19:18:09 Duration:    546.557462ms
 
2020/11/17 19:18:09 Values to read 1000000
2020/11/17 19:18:09 Read 1000000 values active 500000 inactive 500000
2020/11/17 19:18:09 Start time:  2020-11-17 19:18:09.145480412 +0100 CET m=+1.129773875
2020/11/17 19:18:09 End time:    2020-11-17 19:18:09.743330813 +0100 CET m=+1.727624282
2020/11/17 19:18:09 Duration:    597.850522ms
Poznámka: povšimněte si, že i při čtení po jednotlivých záznamech jsme dosáhli času přibližně 0,6 sekundy, což je v porovnání s přibližně 24 sekundami pro celou databázi skutečné zrychlení ukazující jednu z předností sloupcových databází.

15. Specifikace čteného sloupce jeho jménem (cestou)

V dalším – již předposledním – demonstračním příkladu je ukázáno, jak lze specifikovat sloupec cestou. Zde je situace nepatrně složitější, protože v cestě není uveden jen název sloupce, ale i případné jméno souboru obsahujícího tento sloupec (teoreticky je totiž možné tabulku rozdělit do většího množství souborů):

const activeColumnPath = "parquet_go_root.active"

Čtení potom může vypadat následovně:

for i := 0; i < valuesCount; i++ {
        // try to read value
        data, _, _, err := parquetReader.ReadColumnByPath(activeColumnPath, 1)
        if err != nil {
                log.Println("Read error", err)
                continue
        } else {
                // ...
        }
        if data[0].(bool) {
                activeCount++
        } else {
                inactiveCount++
        }
}

Úplný zdrojový kód tohoto demonstračního příkladu:

// This tool is able to read all records stored in selected Parquet file.
// Currently, only records with the structure `Record` is read correctly. Name
// of input Parquet file needs to be selected from command line.
package main
 
import (
        "log"
        "time"
 
        "github.com/xitongsys/parquet-go-source/local"
        "github.com/xitongsys/parquet-go/reader"
        "github.com/xitongsys/parquet-go/source"
)
 
// Record represents one record stored in Parquet file
type Record struct {
        ID      uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"`
        Name    string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"`
        Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"`
        Email   string `parquet:"name=email, type=UTF8, encoding=PLAIN"`
        Active  bool   `parquet:"name=active, type=BOOLEAN"`
        Color   string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"`
}
 
const activeColumnPath = "parquet_go_root.active"
 
// closeReader tries to close the given Parquet file reader
func closeReader(reader source.ParquetFile) {
        err := reader.Close()
        if err != nil {
                log.Println("close reader:", err)
        }
}
 
func readParquetFile(fileName string) {
        t1 := time.Now()
 
        const parallelNumber = 1
 
        // construct the file reader and try to open the Parquet file for
        // reading
        fileReader, err := local.NewLocalFileReader(fileName)
 
        if err != nil {
                log.Fatal("Can't open file", err)
                return
        }
 
        // fileReader needs to be closed properly
        defer closeReader(fileReader)
 
        // initialize Parquet file reader
        parquetColumnReader, err := reader.NewParquetColumnReader(fileReader, parallelNumber)
 
        if err != nil {
                log.Fatal("Can't create parquet column reader", err)
                return
        }
 
        // parquetReader needs to be stopped
        defer parquetColumnReader.ReadStop()
 
        readColumnData(parquetColumnReader, activeColumnPath)
 
        // compute and print duration
        t2 := time.Now()
        since := time.Since(t1)
        log.Println("Start time: ", t1)
        log.Println("End time:   ", t2)
        log.Println("Duration:   ", since)
}
 
func readColumnData(parquetReader *reader.ParquetReader, columnPath string) {
        valuesCount := int(parquetReader.GetNumRows())
        log.Println("Values to read", valuesCount)
 
        activeCount := 0
        inactiveCount := 0
 
        values := 0
 
        // try to read and display all records
        for i := 0; i < valuesCount; i++ {
                // try to read value
                data, _, _, err := parquetReader.ReadColumnByPath(columnPath, 1)
                if err != nil {
                        log.Println("Read error", err)
                        continue
                } else {
                        values++
                }
                if data[0].(bool) {
                        activeCount++
                } else {
                        inactiveCount++
                }
        }
        log.Println("Read", values, "values", "active", activeCount, "inactive", inactiveCount)
}
 
func main() {
        readParquetFile("1000000records_compression_none.parquet")
        readParquetFile("1000000records_compression_snappy.parquet")
        readParquetFile("1000000records_compression_gzip.parquet")
}
Poznámka: časy čtení zde neuvádím, protože jsou prakticky totožné s časy, které jsme získali předchozím demonstračním příkladem.

16. Rychlost přečtení všech údajů z jediného sloupce

I když je čtení dat z jediného sloupce (podle očekávání) rychlejší, než zpracování databáze po řádcích, je možné ho urychlit čtením po větších blocích, což je postup, který již velmi dobře známe. Pro naše účely tento postup nepatrně upravíme:

for readValues < valuesCount {
        // try to read value
        data, _, _, err := parquetReader.ReadColumnByIndex(int64(columnIndex), int64(blockSize))
        if err != nil {
                log.Println("Read error", err)
                continue
        }
        for _, active := range data {
                if active.(bool) {
                        activeCount++
                } else {
                        inactiveCount++
                }
        }
}
Poznámka: povšimněte si, že se v tomto případě z metody parquetReader.ReadColumnByIndex vrátil řez o maximálním počtu prvků odpovídající hodnotě blockSize. Poslední blok opět může být menší, což je pochopitelné (počet řádků nemusí odpovídat celočíselnému násobku velikosti bloku).

V dnešním posledním příkladu se přečte celý sloupec s využitím bloků proměnné délky a současně i při použití jedné, osmi, šestnácti, popř. 32 gorutin, v jejichž kódu je realizován kód pro čtení:

// This tool is able to read all records stored in selected Parquet file.
// Currently, only records with the structure `Record` is read correctly. Name
// of input Parquet file needs to be selected from command line.
package main
 
import (
        "encoding/csv"
        "fmt"
        "log"
        "os"
        "strconv"
        "time"

        "github.com/xitongsys/parquet-go-source/local"
        "github.com/xitongsys/parquet-go/reader"
        "github.com/xitongsys/parquet-go/source"
)
 
// maximum block size for reading Parquet files by blocks
const maxBlockSize = 100
 
// Record represents one record stored in Parquet file
type Record struct {
        ID      uint64 `parquet:"name=id, type=UINT_64, encoding=PLAIN"`
        Name    string `parquet:"name=name, type=UTF8, encoding=PLAIN_DICTIONARY"`
        Surname string `parquet:"name=surname, type=UTF8, encoding=PLAIN"`
        Email   string `parquet:"name=email, type=UTF8, encoding=PLAIN"`
        Active  bool   `parquet:"name=active, type=BOOLEAN"`
        Color   string `parquet:"name=color, type=UTF8, encoding=PLAIN_DICTIONARY"`
}
 
const activeColumnIndex = 4
 
// closeReader tries to close the given Parquet file reader
func closeReader(reader source.ParquetFile) {
        err := reader.Close()
        if err != nil {
                log.Println("close reader:", err)
        }
}
 
func readParquetFile(fileName string, blockSize int, readers int) {
        // construct the file reader and try to open the Parquet file for
        // reading
        fileReader, err := local.NewLocalFileReader(fileName)
 
        if err != nil {
                log.Fatal("Can't open file", err)
                return
        }
 
        // fileReader needs to be closed properly
        defer closeReader(fileReader)
 
        // initialize Parquet file reader
        parquetColumnReader, err := reader.NewParquetColumnReader(fileReader, int64(readers))
 
        if err != nil {
                log.Fatal("Can't create parquet column reader", err)
                return
        }
 
        // parquetReader needs to be stopped
        defer parquetColumnReader.ReadStop()
 
        readColumnData(parquetColumnReader, activeColumnIndex, blockSize)

}
 
func readColumnData(parquetReader *reader.ParquetReader, columnIndex int, blockSize int) {
        valuesCount := int(parquetReader.GetNumRows())
 
        activeCount := 0
        inactiveCount := 0
 
        readValues := 0
 
        // try to read and display all records
        for readValues < valuesCount {
                // try to read value
                data, _, _, err := parquetReader.ReadColumnByIndex(int64(columnIndex), int64(blockSize))
                if err != nil {
                        log.Println("Read error", err)
                        continue
                } else {
                        readValues += len(data)
                }
                for _, active := range data {
                        if active.(bool) {
                                activeCount++
                        } else {
                                inactiveCount++
                        }
                }
        }
        log.Println("Read", readValues, "values", "active", activeCount, "inactive", inactiveCount)
}
 
func main() {
        var readers []int = []int{1, 8, 16, 32}
 
        for _, numReaders := range readers {
                // create and open new CSV file
                csvFileName := fmt.Sprintf("durations-%d-readers.csv", numReaders)
 
                csvFile, err := os.Create(csvFileName)
                if err != nil {
                        log.Fatal("Create CSV file", err)
                }
 
                defer csvFile.Close()
 
                // initialize CSV writer
                csvWriter := csv.NewWriter(csvFile)
                defer csvWriter.Flush()
 
                csvWriter.Write([]string{"Block size", "Time to read"})
 
                for blockSize := 1; blockSize <= maxBlockSize; blockSize++ {
                        t1 := time.Now()
 
                        readParquetFile("1000000records_compression_none.parquet", blockSize, numReaders)
 
                        // compute and print duration
                        since := time.Since(t1)
                        log.Printf("Block size: %d  Readers: %d  Duration: %d\n", blockSize, numReaders, since)
 
                        // write duration into CSV file
                        csvWriter.Write([]string{strconv.Itoa(blockSize), strconv.Itoa(int(since))})
                }
        }
}

17. Výsledky měření rychlosti

Podívejme se nyní na dosažené časy. Z nich je patrné, že i relativně malá velikost bloku (řekněme šedesát prvků) vede ke znatelnému urychlení načítání hodnot ze sloupce:

Obrázek 6: Rychlost čtení údajů z jediného sloupce pro různé velikosti bloků a různý počet gorutin.

Poznámka: v tomto případě nemá počet gorutin prakticky žádný význam, a to z toho důvodu, že sloupec obsahuje jen primitivní hodnoty, takže kód pro načtení je dostatečně malý a rychlý.

18. Pomocné skripty pro tvorbu grafů

Jen pro úplnost si uveďme, jaké skripty byly použity pro přípravu grafů pro dnešní článek.

První skript načte CSV soubor s dvojicí sloupců – velikost bloku a čas přečtení všech záznamů, popř. hodnot z Parquet souboru. Z těchto údajů vytvoří jednoduchý graf, který je zobrazen a současně i uložen (rastrový obrázek PNG + vektorová kresba SVG). Využívá se možností knihovny Matplotlib:

#!/usr/bin/env python3
 
import sys
import csv
import matplotlib.pyplot as plt
 
# Check if command line argument is specified (it is mandatory).
if len(sys.argv) < 2:
    print("Usage:")
    print("  read-by-blocks-chart.py input_file.csv")
    print("Example:")
    print("  read-by-blocks-chart.py durations.csv")
    sys.exit(1)
 
# First command line argument should contain name of input CSV.
input_csv = sys.argv[1]
 
# Try to open the CSV file specified.
with open(input_csv) as csv_input:
    # And open this file as CSV
    csv_reader = csv.reader(csv_input)
 
    # Skip header
    next(csv_reader, None)
 
    # Read all rows from the provided CSV file
    durations = [(int(row[0]), int(row[1])) for row in csv_reader]
 
# Create new graph
x = [i[0] for i in durations]
y = [i[1] for i in durations]
 
plt.plot(x, y, "b")
 
# Title of a graph
plt.title("Reading by block with size N")
 
# Add a label to x-axis
plt.xlabel("Block size")
 
# Add a label to y-axis
plt.ylabel("Duration time [ns]")
 
# Set the plot layout
plt.tight_layout()
 
# And save the plot into raster format and vector format as well
plt.savefig("read-by-block-time.png")
plt.savefig("read-by-block-time.svg")
 
# Try to show the plot on screen
plt.show()

Druhý skript je velmi podobný skriptu prvnímu, ovšem odlišnost spočívá v tom, že načte dva CSV soubory a vykreslí graf s dvojicí průběhů, které je tak možné snadno porovnat:

#!/usr/bin/env python3
 
import sys
import csv
import matplotlib.pyplot as plt
 
 
def read_csv(filename):
    # Try to open the CSV file specified.
    with open(filename) as csv_input:
        # And open this file as CSV
        csv_reader = csv.reader(csv_input)
 
        # Skip header
        next(csv_reader, None)
 
        # Read all rows from the provided CSV file
        durations = [(int(row[0]), int(row[1])) for row in csv_reader]
 
    # Create new graph
    x = [i[0] for i in durations]
    y = [i[1] for i in durations]
 
    return x, y
 
 
# Check if command line argument is specified (it is mandatory).
if len(sys.argv) < 3:
    print("Usage:")
    print("  read-by-blocks-charts.py input_file.csv input_file.csv")
    print("Example:")
    print("  read-by-blocks-charts.py durations-1.csv durations-100.csv")
    sys.exit(1)
 
# First command line argument should contain name of input CSV.
input_csv_1 = sys.argv[1]
input_csv_2 = sys.argv[2]
 
x1, y1 = read_csv(input_csv_1)
x2, y2 = read_csv(input_csv_2)
 
plt.plot(x1, y1, "b", label="1 reader goroutine")
plt.plot(x2, y2, "r", label="100 reader goroutines")
 
# Title of a graph
plt.title("Reading by block with size N")
 
# Add a label to x-axis
plt.xlabel("Block size")
 
# Add a label to y-axis
plt.ylabel("Duration time [ns]")
 
# Add a legend
plt.legend()
 
# Set the plot layout
plt.tight_layout()
 
# And save the plot into raster format and vector format as well
plt.savefig("read-by-block-time.png")
plt.savefig("read-by-block-time.svg")
 
# Try to show the plot on screen
plt.show()

19. Repositář s demonstračními příklady

Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:

bitcoin_skoleni

# Příklad Stručný popis Cesta
1 01-write-performance-by-records měření rychlosti zápisu do Parquet souborů po záznamech https://github.com/tisnik/go-root/blob/master/article68/09-write-performance/01-write-performance-by-records
2 02-read-performance-by-records měření rychlosti čtení z Parquet souborů po záznamech https://github.com/tisnik/go-root/blob/master/article69/02-read-performance-by-records
3 03-read-performance-by-blocks měření rychlosti čtení z Parquet souborů po blocích https://github.com/tisnik/go-root/blob/master/article69/03-read-performance-by-blocks
4 04-write-performance-by-records-pprof měření rychlosti čtení z Parquet souborů po záznamech + informace z profileru https://github.com/tisnik/go-root/blob/master/article69/04-write-performance-by-records-pprof
5 05-plot-read-performance-by-blocks vytvoření CSV souboru s údaji o rychlosti čtení z Parquet souboru https://github.com/tisnik/go-root/blob/master/article69/05-plot-read-performance-by-blocks
6 06-plot-read-performance-by-blocks-100-readers čtení z Parquet souborů s využitím 100 gorutin https://github.com/tisnik/go-root/blob/master/article69/06-plot-read-performance-by-blocks-100-readers
7 07-plot-read-performance-by-block-N-readers čtení z Parquet souborů s využitím proměnného počtu gorutin https://github.com/tisnik/go-root/blob/master/article69/07-plot-read-performance-by-block-N-readers
8 08-read-performance-by-column-index čtení hodnot ze sloupce vybraného jeho indexem https://github.com/tisnik/go-root/blob/master/article69/08-read-performance-by-column-index
9 09-read-performance-by-column-path čtení hodnot ze sloupce vybraného „cestou“ https://github.com/tisnik/go-root/blob/master/article69/09-read-performance-by-column-path
10 10-plot-read-performance-by-column-index změření rychlosti čtení hodnot z vybraného sloupce https://github.com/tisnik/go-root/blob/master/article69/10-plot-read-performance-by-column-index

Pomocné nástroje:

# Skript Stručný popis Cesta
1 read-by-blocks-chart.py vytvoření grafu rychlosti načítání dat v závislosti na velikosti bloku https://github.com/tisnik/go-root/blob/master/tools/read-by-blocks-chart.py
2 read-by-blocks-charts.py vytvoření grafů s větším množstvím průběhů https://github.com/tisnik/go-root/blob/master/tools/read-by-blocks-charts.py

Výsledky všech měření ve formě CSV souborů:

# Soubor Stručný popis Cesta
1 durations-1.csv přečtení všech záznamů v jediné gorutině https://github.com/tisnik/go-root/blob/master/article69/re­sults/durations-1.csv
2 durations-100.csv přečtení všech záznamů ve 100 gorutinách https://github.com/tisnik/go-root/blob/master/article69/re­sults/durations-100.csv
3 durations-N.csv přečtení všech záznamů v proměnném počtu gorutin https://github.com/tisnik/go-root/blob/master/article69/re­sults/durations-N.csv
4 durations-column-reader-1-readers.csv přečtení obsahu sloupce v jedné gorutině https://github.com/tisnik/go-root/blob/master/article69/re­sults/durations-column-reader-1-readers.csv
5 durations-column-reader-8-readers.csv přečtení obsahu sloupce v 8 gorutinách https://github.com/tisnik/go-root/blob/master/article69/re­sults/durations-column-reader-8-readers.csv
6 durations-column-reader-16-readers.csv přečtení obsahu sloupce v 16 gorutinách https://github.com/tisnik/go-root/blob/master/article69/re­sults/durations-column-reader-16-readers.csv
7 durations-column-reader-32-readers.csv přečtení obsahu sloupce v 32 gorutinách https://github.com/tisnik/go-root/blob/master/article69/re­sults/durations-column-reader-32-readers.csv

20. Odkazy na Internetu

  1. Několik poznámek ke sloupcovým databázím
    https://www.root.cz/clanky/nekolik-poznamek-ke-sloupcovym-databazim/
  2. Column-oriented DBMS (Wikipedia)
    https://en.wikipedia.org/wiki/Column-oriented_DBMS
  3. Extract, transform, load (ETL)
    https://en.wikipedia.org/wi­ki/Extract,_transform,_lo­ad
  4. Top 9 column-oriented databases
    https://www.predictiveana­lyticstoday.com/top-wide-columnar-store-databases/
  5. Apache Parquet
    https://parquet.apache.org/
  6. Parquet format
    https://github.com/apache/parquet-format
  7. Processing parquet files in Golang
    https://dev.to/eminetto/processing-parquet-files-in-golang-1nni
  8. Processing parquet files in Golang
    https://eltonminetto.dev/en/post/2019–12–09-parquet-golang/
  9. Converting CSV files to Parquet with Go
    https://mungingdata.com/go/csv-to-parquet/
  10. Balíček parquet-go
    https://github.com/xitongsys/parquet-go
  11. Balíček parquet
    https://github.com/parsyl/parquet
  12. Dokumentace k balíčku parquet-go
    https://godoc.org/github.com/xi­tongsys/parquet-go
  13. Parquet File Format Hadoop
    https://acadgild.com/blog/parquet-file-format-hadoop
  14. What is Apache Parquet and why you should use it
    https://www.upsolver.com/blog/apache-parquet-why-use
  15. Structure Of Parquet File Format
    https://www.ellicium.com/parquet-file-format-structure/
  16. Parquet File with Example
    https://commandstech.com/parquet-with-example/
  17. Faker
    https://github.com/bxcodec/faker/
  18. Apache ORC – the smallest, fastest columnar storage for Hadoop workloads
    https://orc.apache.org/
  19. Apache Parquet (Wikipedia)
    https://en.wikipedia.org/wi­ki/Apache_Parquet
  20. Apache ORC (Wikipedia)
    https://en.wikipedia.org/wi­ki/Apache_ORC
  21. MonetDB
    https://www.monetdb.org/
  22. Future of Column-Oriented Data Processing with Arrow & Parquet by Julien Le Dem | DataEngConf NY '16
    https://www.youtube.com/wat­ch?v=6lCVKMQR8Dw
  23. Data Architecture 101 for Your Business
    https://www.youtube.com/wat­ch?v=ArzohefZLE4
  24. Functional Data Engineering – A Set of Best Practices | Lyft
    https://www.youtube.com/wat­ch?v=4Spo2QRTz1k
  25. Go Data Structures: Binary Search Tree
    https://flaviocopes.com/golang-data-structure-binary-search-tree/
  26. Gobs of data
    https://blog.golang.org/gobs-of-data
  27. Formát BSON
    http://bsonspec.org/
  28. Golang Guide: A List of Top Golang Frameworks, IDEs & Tools
    https://blog.intelligentbe­e.com/2017/08/14/golang-guide-list-top-golang-frameworks-ides-tools/
  29. Stránky projektu MinIO
    https://min.io/
  30. MinIO Quickstart Guide
    https://docs.min.io/docs/minio-quickstart-guide.html
  31. MinIO Go Client API Reference
    https://docs.min.io/docs/golang-client-api-reference
  32. MinIO Python Client API Reference
    https://docs.min.io/docs/python-client-api-reference.html
  33. Performance at Scale: MinIO Pushes Past 1.4 terabits per second with 256 NVMe Drives
    https://blog.min.io/performance-at-scale-minio-pushes-past-1–3-terabits-per-second-with-256-nvme-drives/
  34. Benchmarking MinIO vs. AWS S3 for Apache Spark
    https://blog.min.io/benchmarking-apache-spark-vs-aws-s3/
  35. MinIO Client Quickstart Guide
    https://docs.min.io/docs/minio-client-quickstart-guide.html
  36. Analýza kvality zdrojových kódů Minia
    https://goreportcard.com/re­port/github.com/minio/minio
  37. This is MinIO
    https://www.youtube.com/wat­ch?v=vF0lQh0XOCs
  38. Running MinIO Standalone
    https://www.youtube.com/wat­ch?v=dIQsPCHvHoM
  39. „Amazon S3 Compatible Storage in Kubernetes“ – Rob Girard, Principal Tech Marketing Engineer, Minio
    https://www.youtube.com/wat­ch?v=wlpn8K0jJ4U
  40. Metric types
    https://prometheus.io/doc­s/concepts/metric_types/
  41. Histograms with Prometheus: A Tale of Woe
    http://linuxczar.net/blog/2017/06/15/pro­metheus-histogram-2/
  42. Why are Prometheus histograms cumulative?
    https://www.robustperception.io/why-are-prometheus-histograms-cumulative
  43. Histograms and summaries
    https://prometheus.io/doc­s/practices/histograms/
  44. Instrumenting Golang server in 5 min
    https://medium.com/@gsisi­mogang/instrumenting-golang-server-in-5-min-c1c32489add3
  45. Semantic Import Versioning in Go
    https://www.aaronzhuo.com/semantic-import-versioning-in-go/
  46. Sémantické verzování
    https://semver.org/
  47. Getting started with Go modules
    https://medium.com/@fonse­ka.live/getting-started-with-go-modules-b3dac652066d
  48. Create projects independent of $GOPATH using Go Modules
    https://medium.com/mindorks/create-projects-independent-of-gopath-using-go-modules-802260cdfb51o
  49. Anatomy of Modules in Go
    https://medium.com/rungo/anatomy-of-modules-in-go-c8274d215c16
  50. Modules
    https://github.com/golang/go/wi­ki/Modules
  51. Go Modules Tutorial
    https://tutorialedge.net/golang/go-modules-tutorial/
  52. Module support
    https://golang.org/cmd/go/#hdr-Module_support
  53. Go Lang: Memory Management and Garbage Collection
    https://vikash1976.wordpres­s.com/2017/03/26/go-lang-memory-management-and-garbage-collection/
  54. Golang Internals, Part 4: Object Files and Function Metadata
    https://blog.altoros.com/golang-part-4-object-files-and-function-metadata.html
  55. A StreamLike, Immutable, Lazy Loading and smart Golang Library to deal with slices
    https://github.com/wesovilabs/koazee
  56. Handling Sparse Files on Linux
    https://www.systutorials.com/136652/han­dling-sparse-files-on-linux/
  57. Gzip (Wikipedia)
    https://en.wikipedia.org/wiki/Gzip
  58. Deflate
    https://en.wikipedia.org/wiki/DEFLATE
  59. Rozhraní io.ByteReader
    https://golang.org/pkg/io/#ByteReader
  60. Rozhraní io.RuneReader
    https://golang.org/pkg/io/#RuneReader
  61. Rozhraní io.ByteScanner
    https://golang.org/pkg/io/#By­teScanner
  62. Rozhraní io.RuneScanner
    https://golang.org/pkg/io/#Ru­neScanner
  63. Rozhraní io.Closer
    https://golang.org/pkg/io/#Closer
  64. Rozhraní io.Reader
    https://golang.org/pkg/io/#Reader
  65. Rozhraní io.Writer
    https://golang.org/pkg/io/#Writer
  66. Typ Strings.Reader
    https://golang.org/pkg/strin­gs/#Reader
  67. VACUUM (SQL)
    https://www.sqlite.org/lan­g_vacuum.html
  68. VACUUM (Postgres)
    https://www.postgresql.or­g/docs/8.4/sql-vacuum.html
  69. The Go Programming Language (home page)
    https://golang.org/
  70. GoDoc
    https://godoc.org/
  71. Go (programming language), Wikipedia
    https://en.wikipedia.org/wi­ki/Go_(programming_langua­ge)
  72. Go Books (kniha o jazyku Go)
    https://github.com/dariubs/GoBooks
  73. The Go Programming Language Specification
    https://golang.org/ref/spec
  74. Go: the Good, the Bad and the Ugly
    https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/
  75. Package builtin
    https://golang.org/pkg/builtin/
  76. The Little Go Book (další kniha)
    https://github.com/dariubs/GoBooks
  77. The Go Programming Language by Brian W. Kernighan, Alan A. A. Donovan
    https://www.safaribookson­line.com/library/view/the-go-programming/9780134190570/e­book_split010.html
  78. Learning Go
    https://www.miek.nl/go/
  79. Go Bootcamp
    http://www.golangbootcamp.com/
  80. Programming in Go: Creating Applications for the 21st Century (další kniha o jazyku Go)
    http://www.informit.com/sto­re/programming-in-go-creating-applications-for-the-21st-9780321774637
  81. Introducing Go (Build Reliable, Scalable Programs)
    http://shop.oreilly.com/pro­duct/0636920046516.do
  82. Learning Go Programming
    https://www.packtpub.com/application-development/learning-go-programming
  83. The Go Blog
    https://blog.golang.org/
  84. Getting to Go: The Journey of Go's Garbage Collector
    https://blog.golang.org/ismmkeynote
  85. Go (programovací jazyk, Wikipedia)
    https://cs.wikipedia.org/wi­ki/Go_(programovac%C3%AD_ja­zyk)
  86. Installing Go on the Raspberry Pi
    https://dave.cheney.net/2012/09/25/in­stalling-go-on-the-raspberry-pi
  87. How the Go runtime implements maps efficiently (without generics)
    https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics
  88. Niečo málo o Go – Golang (slovensky)
    http://golangsk.logdown.com/
  89. How Many Go Developers Are There?
    https://research.swtch.com/gop­hercount
  90. Modern garbage collection: A look at the Go GC strategy
    https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e
  91. Go GC: Prioritizing low latency and simplicity
    https://blog.golang.org/go15gc
  92. Is Golang a good language for embedded systems?
    https://www.quora.com/Is-Golang-a-good-language-for-embedded-systems
  93. How to use databases with Golang
    https://hackernoon.com/how-to-work-with-databases-in-golang-33b002aa8c47

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.