241 lines
6.1 KiB
Go
241 lines
6.1 KiB
Go
|
|
// /futriis/internal/storage/compression.go
|
|||
|
|
// Пакет storage реализует простейшее сжатие для колонок с одинаковыми типами данных
|
|||
|
|
|
|||
|
|
package storage
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/binary"
|
|||
|
|
"math"
|
|||
|
|
"strconv"
|
|||
|
|
"sync/atomic"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// CompressionType тип сжатия
|
|||
|
|
type CompressionType int
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
NoCompression CompressionType = iota
|
|||
|
|
RLECompression // Run-length encoding для повторяющихся значений
|
|||
|
|
DeltaCompression // Дельта-сжатие для чисел
|
|||
|
|
DictionaryCompression // Словарное сжатие для строк
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ColumnCompressor предоставляет сжатие для колонки
|
|||
|
|
type ColumnCompressor struct {
|
|||
|
|
colType string
|
|||
|
|
compType CompressionType
|
|||
|
|
stats struct {
|
|||
|
|
originalSize int64
|
|||
|
|
compressedSize int64
|
|||
|
|
savings int64
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewColumnCompressor создаёт новый компрессор для колонки
|
|||
|
|
func NewColumnCompressor(colType string) *ColumnCompressor {
|
|||
|
|
cc := &ColumnCompressor{
|
|||
|
|
colType: colType,
|
|||
|
|
compType: NoCompression,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Автоматически выбираем тип сжатия на основе типа данных
|
|||
|
|
switch colType {
|
|||
|
|
case "int", "int64", "float64":
|
|||
|
|
cc.compType = DeltaCompression
|
|||
|
|
case "string":
|
|||
|
|
cc.compType = DictionaryCompression
|
|||
|
|
default:
|
|||
|
|
cc.compType = RLECompression
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return cc
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Compress сжимает данные
|
|||
|
|
func (cc *ColumnCompressor) Compress(data []interface{}) ([]byte, error) {
|
|||
|
|
var compressed []byte
|
|||
|
|
var err error
|
|||
|
|
|
|||
|
|
switch cc.compType {
|
|||
|
|
case RLECompression:
|
|||
|
|
compressed, err = cc.rleCompress(data)
|
|||
|
|
case DeltaCompression:
|
|||
|
|
compressed, err = cc.deltaCompress(data)
|
|||
|
|
case DictionaryCompression:
|
|||
|
|
compressed, err = cc.dictionaryCompress(data)
|
|||
|
|
default:
|
|||
|
|
// Без сжатия - просто сериализуем
|
|||
|
|
compressed, err = cc.noCompress(data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Обновляем статистику
|
|||
|
|
originalSize := int64(len(data) * 8) // Примерная оценка
|
|||
|
|
compressedSize := int64(len(compressed))
|
|||
|
|
|
|||
|
|
atomic.AddInt64(&cc.stats.originalSize, originalSize)
|
|||
|
|
atomic.AddInt64(&cc.stats.compressedSize, compressedSize)
|
|||
|
|
atomic.AddInt64(&cc.stats.savings, originalSize-compressedSize)
|
|||
|
|
|
|||
|
|
return compressed, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// rleCompress реализует сжатие повторяющихся значений
|
|||
|
|
func (cc *ColumnCompressor) rleCompress(data []interface{}) ([]byte, error) {
|
|||
|
|
if len(data) == 0 {
|
|||
|
|
return []byte{}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result := make([]byte, 0)
|
|||
|
|
|
|||
|
|
current := data[0]
|
|||
|
|
count := 1
|
|||
|
|
|
|||
|
|
for i := 1; i < len(data); i++ {
|
|||
|
|
if data[i] == current {
|
|||
|
|
count++
|
|||
|
|
} else {
|
|||
|
|
// Записываем значение и счётчик
|
|||
|
|
result = append(result, []byte(encodeValue(current))...)
|
|||
|
|
result = append(result, byte(count))
|
|||
|
|
|
|||
|
|
current = data[i]
|
|||
|
|
count = 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Записываем последнее значение
|
|||
|
|
result = append(result, []byte(encodeValue(current))...)
|
|||
|
|
result = append(result, byte(count))
|
|||
|
|
|
|||
|
|
return result, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// deltaCompress реализует дельта-сжатие для чисел
|
|||
|
|
func (cc *ColumnCompressor) deltaCompress(data []interface{}) ([]byte, error) {
|
|||
|
|
if len(data) == 0 {
|
|||
|
|
return []byte{}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result := make([]byte, 8) // Первое значение храним полностью
|
|||
|
|
|
|||
|
|
// Преобразуем первое значение
|
|||
|
|
first, ok := data[0].(float64)
|
|||
|
|
if !ok {
|
|||
|
|
if i, ok := data[0].(int); ok {
|
|||
|
|
first = float64(i)
|
|||
|
|
} else {
|
|||
|
|
return cc.noCompress(data)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
binary.LittleEndian.PutUint64(result, math.Float64bits(first))
|
|||
|
|
|
|||
|
|
// Для остальных храним дельты
|
|||
|
|
for i := 1; i < len(data); i++ {
|
|||
|
|
var curr float64
|
|||
|
|
switch v := data[i].(type) {
|
|||
|
|
case float64:
|
|||
|
|
curr = v
|
|||
|
|
case int:
|
|||
|
|
curr = float64(v)
|
|||
|
|
default:
|
|||
|
|
return cc.noCompress(data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
prev, _ := data[i-1].(float64)
|
|||
|
|
if iPrev, ok := data[i-1].(int); ok {
|
|||
|
|
prev = float64(iPrev)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
delta := int16(curr - prev)
|
|||
|
|
deltaBytes := make([]byte, 2)
|
|||
|
|
binary.LittleEndian.PutUint16(deltaBytes, uint16(delta))
|
|||
|
|
result = append(result, deltaBytes...)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// dictionaryCompress реализует словарное сжатие для строк
|
|||
|
|
func (cc *ColumnCompressor) dictionaryCompress(data []interface{}) ([]byte, error) {
|
|||
|
|
// Строим словарь уникальных значений
|
|||
|
|
dict := make(map[string]byte)
|
|||
|
|
values := make([]byte, len(data))
|
|||
|
|
|
|||
|
|
nextCode := byte(0)
|
|||
|
|
|
|||
|
|
for i, val := range data {
|
|||
|
|
str, ok := val.(string)
|
|||
|
|
if !ok {
|
|||
|
|
return cc.noCompress(data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
code, exists := dict[str]
|
|||
|
|
if !exists {
|
|||
|
|
code = nextCode
|
|||
|
|
dict[str] = code
|
|||
|
|
nextCode++
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
values[i] = code
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Кодируем: сначала словарь, затем значения
|
|||
|
|
result := make([]byte, 0)
|
|||
|
|
|
|||
|
|
// Записываем размер словаря
|
|||
|
|
result = append(result, byte(len(dict)))
|
|||
|
|
|
|||
|
|
// Записываем словарь
|
|||
|
|
for str, code := range dict {
|
|||
|
|
result = append(result, code)
|
|||
|
|
result = append(result, byte(len(str)))
|
|||
|
|
result = append(result, []byte(str)...)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Записываем значения
|
|||
|
|
result = append(result, values...)
|
|||
|
|
|
|||
|
|
return result, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// noCompress без сжатия
|
|||
|
|
func (cc *ColumnCompressor) noCompress(data []interface{}) ([]byte, error) {
|
|||
|
|
result := make([]byte, 0)
|
|||
|
|
for _, val := range data {
|
|||
|
|
result = append(result, []byte(encodeValue(val))...)
|
|||
|
|
}
|
|||
|
|
return result, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// encodeValue кодирует значение в строку
|
|||
|
|
func encodeValue(val interface{}) string {
|
|||
|
|
switch v := val.(type) {
|
|||
|
|
case string:
|
|||
|
|
return v
|
|||
|
|
case int:
|
|||
|
|
return strconv.Itoa(v)
|
|||
|
|
case int64:
|
|||
|
|
return strconv.FormatInt(v, 10)
|
|||
|
|
case float64:
|
|||
|
|
return strconv.FormatFloat(v, 'f', -1, 64)
|
|||
|
|
case bool:
|
|||
|
|
return strconv.FormatBool(v)
|
|||
|
|
default:
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetStats возвращает статистику сжатия
|
|||
|
|
func (cc *ColumnCompressor) GetStats() map[string]int64 {
|
|||
|
|
return map[string]int64{
|
|||
|
|
"original_size": atomic.LoadInt64(&cc.stats.originalSize),
|
|||
|
|
"compressed_size": atomic.LoadInt64(&cc.stats.compressedSize),
|
|||
|
|
"savings": atomic.LoadInt64(&cc.stats.savings),
|
|||
|
|
}
|
|||
|
|
}
|