Go: ускоряем выборку больших таблиц из MySQL

    Я использую Go для написания рекламной сети вот уже почти год. Разработку веду на сервере Intel i7-7700, 16Gb RAM, 256Gb SSD. И в скрипте который выполняется раз в сутки появилась задача выбрать все показы за прошедшие сутки и пересчитать на этой основе статистику за день сразу по нескольким объектам (сайт, кампания, баннер).

    По идиомам Go делается всё достаточно тривиально:

    type Hit struct {
    	siteID, zoneID, poolID, mediaID, campaignID uint32
    }
    rows, err := db.Query("SELECT siteID, zoneID, poolID, mediaID, campaignID FROM "+where)
    if err != nil {
    	log.Fatal("Query fail", err)
    }
    defer rows.Close()
    var (
    	c uint32
    	h Hit
    )
    for rows.Next() {
    	rows.Scan(&h.siteID, &h.zoneID, &h.poolID, &h.mediaID, &h.campaignID)
    	campCounter.Inc(h.campaignID)
    	siteCounter.Inc(h.siteID)
    	zoneCounter.Inc(h.zoneID)
    	poolCounter.Inc(h.poolID)
    	mediaCounter.Inc(h.mediaID)
    	c++
    }
    if err := rows.Err(); err != nil {
    	log.Fatal("Scan Rows err", err)
    }
    log.Println(name, " ", c, " ", where, "in", time.Since(now))
    

    Всё работает. И скорость выборки 36 секунд для почти 56 миллионов записей.

    hit_20180507   55928930   time BETWEEN 1525640400 AND 1525726799 in 36.331342451s

    Под капотом анализатора производительности go tool pprof видим примерно следующее

          flat  flat%   sum%        cum   cum%
        7130ms 18.32% 18.32%    10800ms 27.75%  runtime.mallocgc
        2380ms  6.12% 24.43%     5710ms 14.67%  fmt.(*pp).doPrintf
        2140ms  5.50% 29.93%    13300ms 34.17%  github.com/go-sql-driver/mysql.(*textRows).readRow
        1800ms  4.62% 34.56%     2170ms  5.58%  runtime.mapassign_fast32
        1700ms  4.37% 38.93%     1700ms  4.37%  runtime.heapBitsSetType
        1170ms  3.01% 41.93%    36350ms 93.40%  main.loadHits
        1110ms  2.85% 44.78%     8500ms 21.84%  runtime.convT2Eslice
        1070ms  2.75% 47.53%     1970ms  5.06%  fmt.(*fmt).fmt_integer
         950ms  2.44% 49.97%     1380ms  3.55%  github.com/go-sql-driver/mysql.readLengthEncodedString
         930ms  2.39% 52.36%     1060ms  2.72%  runtime.freedefer
         930ms  2.39% 54.75%      930ms  2.39%  runtime.mapaccess1_fast32
         910ms  2.34% 57.09%     2070ms  5.32%  runtime.deferreturn
         860ms  2.21% 59.30%     1220ms  3.13%  runtime.scanobject

    Можно заметить что мы работаем в текстовом протоколе MySQL по mysql.(*textRows).readRow, соответственно пришедшие строки Scan конвертирует в uin32 типы. Но на первом месте по времени у нас функция выделения памяти.

    Что тут можно ускорить?

    Случайно на глаза мне попался тип RawBytes который гарантирует что байты из драйвера базы данных будут переданы пользователю без копирования. Что же. Попытаемся извлечь Scan в промежуточную структуру с полями sql.RawBytes и переконвертируем сами потом []bytes в uint32 с помощью наскоро написанной функции bu2, выбросив проверки на ошибки (ведь вы не станете их искать в пришедшем от БД тексте, да?)

    func b2u(b []byte) uint32 {
    	n := uint32(0)
    	for _, c := range b {
    		n = n*uint32(10) + uint32(c-'0')
    	}
    	return n
    }
    type HitRaw struct {
    	siteID, zoneID, poolID, mediaID, campaignID sql.RawBytes
    }
    

    В итоге время обработки сократилось до 28 секунд, что дает уже чтение 2 миллионов строк в секунду!

    И профайлер даёт уже такую картину

        4690ms 15.68% 15.68%     7630ms 25.51%  runtime.mallocgc
        2400ms  8.02% 23.70%     2700ms  9.03%  runtime.mapaccess1_fast32
        1660ms  5.55% 29.25%     1660ms  5.55%  runtime.heapBitsSetType
        1640ms  5.48% 34.74%    28110ms 93.98%  main.loadHits
        1590ms  5.32% 40.05%     1860ms  6.22%  runtime.mapassign_fast32
        1300ms  4.35% 44.40%    12450ms 41.62%  github.com/go-sql-driver/mysql.(*textRows).readRow
        1140ms  3.81% 48.21%     2090ms  6.99%  runtime.deferreturn
        1060ms  3.54% 51.76%     1470ms  4.91%  github.com/go-sql-driver/mysql.readLengthEncodedString
        1050ms  3.51% 55.27%     1050ms  3.51%  main.b2u
        1040ms  3.48% 58.74%     1130ms  3.78%  database/sql.convertAssign
         910ms  3.04% 61.79%     8640ms 28.89%  runtime.convT2Eslice
         730ms  2.44% 64.23%     2540ms  8.49%  database/sql.(*Rows).Scan

    Что же, неплохо как для начала. Далее я полез изучать драйвер MySQL, который как оказалось написан специально для Go и реализует низкоуровневые протоколы сам, с помощью сокетов. И вот второй протокол MySQL оказался бинарным. Что в теории дает более быструю генерацию ответа MySQL-сервера. Соответственно и драйвер, меньше вызывает функций конвертаций текст-целое число. Чтобы задействовать бинарный протокол надо перейти от db.Query до db.Prepare — stsm.Query — минимум изменений исходного кода и вуаля — 26.70 секунд выполнения.

    stmtOut, err := db.Prepare(sqlQ)
    defer stmtOut.Close()
    if err != nil {
    	log.Fatal("prepare", err, sqlQ)
    }
    rows, err := stmtOut.Query()
    if err != nil {
    	log.Fatal("query", err, sqlQ)
    }
    defer rows.Close()

    Профилировщик показывает, что протокол уже действительно бинарный по (*binaryRows).readRow, но при чтении в RawBytes всё равно проходит конвертация в текст, а потом обратно.

          flat  flat%   sum%        cum   cum%
        2910ms 10.79% 10.79%     3310ms 12.27%  runtime.mallocgc
        2280ms  8.45% 19.24%     2600ms  9.64%  runtime.mapaccess1_fast32
        1960ms  7.27% 26.51%     7070ms 26.21%  database/sql.convertAssign
        1530ms  5.67% 32.18%     1810ms  6.71%  runtime.mapassign_fast32
        1460ms  5.41% 37.60%     6660ms 24.69%  github.com/go-sql-driver/mysql.(*binaryRows).readRow
        1420ms  5.27% 42.86%    26680ms 98.92%  main.loadHits
        1210ms  4.49% 47.35%     3010ms 11.16%  strconv.AppendInt
        1100ms  4.08% 51.43%     1320ms  4.89%  strconv.formatBits
         950ms  3.52% 54.95%     1650ms  6.12%  runtime.deferreturn
         820ms  3.04% 57.99%      820ms  3.04%  reflect.ValueOf
         810ms  3.00% 60.99%     4120ms 15.28%  runtime.convT2E64
         750ms  2.78% 63.77%     4240ms 15.72%  database/sql.asBytes
    

    Давайте же Scan делать сразу в uint32 структуры! Уже ничего не должно конвертироваться — только преобразование целое-целое.

    Итог оказался печальным — 49.827306314s То есть замедление вообще ужасающее. Самый тупящий вариант из всех возможных, несмотря на хорошую теоретическую основу для самого быстрого результата. В чем же дело?

    Смотрим:

       4620ms  9.22%  9.22%    29230ms 58.32%  database/sql.convertAssign
        3610ms  7.20% 16.42%     4010ms  8.00%  runtime.mallocgc
        3010ms  6.01% 22.43%     8610ms 17.18%  reflect.(*rtype).Name
        2980ms  5.95% 28.37%     5600ms 11.17%  reflect.(*rtype).String
        2770ms  5.53% 33.90%     3330ms  6.64%  runtime.mapaccess1_fast32
        2570ms  5.13% 39.03%     2570ms  5.13%  reflect.ValueOf
        1760ms  3.51% 42.54%     1980ms  3.95%  runtime.mapassign_fast32
        1640ms  3.27% 45.81%     6630ms 13.23%  github.com/go-sql-driver/mysql.(*binaryRows).readRow
        1540ms  3.07% 48.88%     3870ms  7.72%  strconv.FormatInt
        1240ms  2.47% 51.36%    49600ms 98.96%  main.loadHits
        1150ms  2.29% 53.65%     1150ms  2.29%  reflect.Value.Type
        1120ms  2.23% 55.89%     1120ms  2.23%  reflect.Value.Elem
        1070ms  2.13% 58.02%    30950ms 61.75%  database/sql.(*Rows).Scan
        1070ms  2.13% 60.16%     1070ms  2.13%  strconv.ParseUint
    

    Судя по наличию strconv.ParseUint — преобразование 2 типов выполняется через строку! Серьезно? reflect-преобразования вышли на первые строчки по времени выполнения. Не зря Роб Пайк говорит об осторожном использовании рефлексии. Можно натворить дел.

    Изучив драйвер MySQL я наткнулся на то, что с бинарного протокола все данные преобразуются в int64 — попробуем извлечь из этого пользу. Scan делаем в структуру

    type HitRaw struct {
    	siteID, zoneID, poolID, mediaID, campaignID int64
    }
    ...
    h.siteID = uint32(raw.siteID)
    h.zoneID = uint32(raw.zoneID)
    h.poolID = uint32(raw.poolID)
    h.mediaID = uint32(raw.mediaID)
    h.campaignID = uint32(raw.campaignID)
    

    Результат получился 33.98 сек. С таким раскладом по функциям

        3600ms 10.48% 10.48%    14360ms 41.79%  database/sql.convertAssign
        2860ms  8.32% 18.80%     3340ms  9.72%  runtime.mallocgc
        2560ms  7.45% 26.25%     2920ms  8.50%  runtime.mapaccess1_fast32
        1660ms  4.83% 31.08%     6730ms 19.59%  github.com/go-sql-driver/mysql.(*binaryRows).readRow
        1540ms  4.48% 35.56%    33970ms 98.86%  main.loadHits
        1410ms  4.10% 39.67%     1690ms  4.92%  runtime.mapassign_fast32
        1340ms  3.90% 43.57%     1340ms  3.90%  reflect.ValueOf
        1290ms  3.75% 47.32%     4010ms 11.67%  reflect.Value.Set
         940ms  2.74% 50.06%    15960ms 46.45%  database/sql.(*Rows).Scan
         900ms  2.62% 52.68%      900ms  2.62%  reflect.Value.Elem
         840ms  2.44% 55.12%      840ms  2.44%  reflect.Value.Type
         840ms  2.44% 57.57%     1500ms  4.37%  runtime.deferreturn
         810ms  2.36% 59.92%      810ms  2.36%  reflect.directlyAssignable
         760ms  2.21% 62.14%      760ms  2.21%  runtime.getitab
         730ms  2.12% 64.26%      900ms  2.62%  reflect.Value.assignTo
         720ms  2.10% 66.36%     4060ms 11.82%  runtime.convT2E64
    

    Видно, что sql.convertAssign уменьшает всю выгоду от использования бинарного протокола. И теперь данные не копируются через текст, но внутри reflect определить что int64 можно копировать в переменную int64 пользователя — ещё довольно сложно. И копирование числа в текст и обратно идет быстрее, чем reflect.directlyAssignable — reflect.Value.assignTo.

    В качестве разминки я попробовал перевести функцию b2u на Go-ассемблер. Ассемблер был моим одним из первых выученных в школе языков программирования на БК-0011 без дисковода и кассетного магнитофона) Так что это было забавно. Хотя Go генерирует практически оптимальный код и если вы не придумаете алгоритмические трики или использование нестандартных команд языка ASM — то смысла особого в написании этих функций нет.

    // func b2u(data []byte) uint32
    //
    // memory layout of the stack relative to FP
    //  +0 data slice ptr
    //  +8 data slice len
    // +16 data slice cap
    #include "textflag.h"
    
    TEXT ·B2u(SB),NOSPLIT,$0-24
      // data ptr
      MOVQ data+0(FP), SI
      // data len
      MOVQ data+8(FP), CX
      // result in AX
      MOVBLZX	(SI), AX
      // - '0' 
      SUBL	$48, AX
      // check end of loop
      DECQ CX
      JZ AX2RET
    
    LOOPBYTE:
      //move to one byte upper
      INCQ  SI
      MOVBLZX	(SI), BX
      //prev result *= 10
      IMULL	$10, AX
      // bx -= '0'
      SUBL	$48, BX
      ADDL	BX, AX
      // check end of loop while (cx--)
      DECQ	CX
      JNZ LOOPBYTE
    	
    AX2RET:
      MOVL AX, ret+24(FP)
      RET
    

    По тестам она даёт ускорение 2-20%, от Go-версии. Зависит от кол-ва цифр в числе.
    В итоге рабочий пример ускорился до 26.94 секунды.

    Вывод из статьи, для тех, кто только просматривал текст — самый быстрый способ прочитать большой объем целочисленных данных из MySQL в память — использовать db.Prepare — stmt.Query — Scan в interface{} и преобразование uint32(hit.siteID.(int64)) в беззнаковое целое число, которое работает и для uint64, не срезая верхний бит.
    То есть, показанные в стандартных примерах способы работы с драйвером не всегда оптимальны. Возможно, разработчики обратят внимание на поведение драйвера, потому что очень много скрытых вызовов и оверхедов как для простого и не перегруженного лишним функционалом языка. Ведь Go в тестах, в которых появляются SELECT-выборки из БД, не блещет производительностью. Мало того, не все опытные программисты имеют время и желание покопаться под капотом. Насколько мне известно, подобные тесты на SELECT вообще никто не проводил.

    UPD: В комментариях привели пример чудодейственного драйвера github.com/lazada/sqle якобы рассчитанного на быстрое чтение. Итог оказался при чтении через
    rows.Scan(&h.siteID, &h.zoneID, &h.poolID, &h.mediaID, &h.campaignID) в uint32 переменные очень печальный
    55928930 time BETWEEN 1525640400 AND 1525726799 in 1m0.307942824s
    И если посмотреть на то, чем занималась минуту программа, становится понятно, что об оптимизации этого случая там просто не задумывались. Стандартный драйвер выигрывает в 2 раза.
    flat flat% sum% cum cum%
    4.63s 7.62% 7.62% 29.25s 48.11% database/sql.convertAssign
    4.22s 6.94% 14.56% 4.68s 7.70% runtime.mallocgc
    2.97s 4.88% 19.44% 8.48s 13.95% reflect.(*rtype).Name
    2.96s 4.87% 24.31% 5.51s 9.06% reflect.(*rtype).String
    2.90s 4.77% 29.08% 41.28s 67.89% github.com/lazada/sqle.(*Rows).Scan
    2.51s 4.13% 33.21% 2.95s 4.85% runtime.mapaccess1_fast32
    2.38s 3.91% 37.12% 2.38s 3.91% reflect.ValueOf
    1.92s 3.16% 40.28% 3.69s 6.07% runtime.assertE2I2
    1.77s 2.91% 43.19% 1.77s 2.91% runtime.getitab
    1.65s 2.71% 45.90% 7.04s 11.58% github.com/go-sql-driver/mysql.(*binaryRows).readRow
    1.49s 2.45% 48.36% 1.86s 3.06% runtime.mapassign_fast32
    1.35s 2.22% 50.58% 60.27s 99.13% main.loadHits
    1.31s 2.15% 52.73% 4.01s 6.60% github.com/lazada/sqle.typeCheck
    1.31s 2.15% 54.88% 4.19s 6.89% strconv.FormatInt
    1.28s 2.11% 56.99% 2.88s 4.74% strconv.formatBits
    1.25s 2.06% 59.05% 1.25s 2.06% reflect.(*rtype).Kind
    1.12s 1.84% 60.89% 31.05s 51.07% database/sql.(*Rows).Scan

    Если читать Scan в []byte переменные и конвертировать через b2u() в uint32 получается 44 секунды. По-моему, дальше можно не тестировать, какой замечательный заменитель стандартного database/sql отписали ребята. Очередной миф про ускорение развенчался об практические тесты.

    UPD2: Для понимания скорости чтения из MySQL сделал цикл без конвертаций и вычислений
    for rows.Next() {
    c++
    }
    

    итог — 11.9 секунд. Таким образом, 15 секунд из 27 тратися на конвертацию и работу с хешмапами.

    UPD3: Написав статью, я стал больше разбираться во внутренностях Go — и нашел пилюлю для своего случая прямо под носом:
    golang.org/pkg/database/sql/#Rows.Scan
    If an argument has type *interface{}, Scan copies the value provided by the underlying driver without conversion.
    Вот финальная версия кода, которую всем рекомендую:
    type HitRaw struct {
    	siteID, zoneID, poolID, mediaID, campaignID, status interface{}
    }
    var (
    	hit HitRaw
    	h   Hit
    )
    for rows.Next() {
    	rows.Scan(&hit.siteID, &hit.zoneID, &hit.poolID, &hit.mediaID, &hit.campaignID, &hit.status)
    
    	h.siteID = uint32(hit.siteID.(int64))
    	h.zoneID = uint32(hit.zoneID.(int64))
    	h.siteID = uint32(hit.siteID.(int64))
    	h.poolID = uint32(hit.poolID.(int64))
    	h.mediaID = uint32(hit.mediaID.(int64))
    	h.campaignID = uint32(hit.campaignID.(int64))
    	h.status = uint8(hit.status.(int64))
    

    Итог не оставит никого равнодушным:
    20.126921479s на техже 55,9 миллионов записей. И это после 27 секунд, на которых я думал, что достиг нирваны.
    И профилировщик выдает уже вполне хороший расклад — большую часть времени программа обновляет счетчики в hash. Бинарные данные отправляются из драйвера прямиком туда, куда их ждут. Ассемблер оказался вовсе не обязателен.
    flat flat% sum% cum cum%
    3110ms 15.20% 15.20% 3460ms 16.91% runtime.mallocgc
    2350ms 11.49% 26.69% 2700ms 13.20% runtime.mapaccess1_fast32
    1850ms 9.04% 35.73% 2150ms 10.51% runtime.mapassign_fast32
    1460ms 7.14% 42.86% 6670ms 32.60% github.com/go-sql-driver/mysql.(*binaryRows).readRow
    1420ms 6.94% 49.80% 20070ms 98.09% main.loadHits
    1170ms 5.72% 55.52% 1190ms 5.82% database/sql.convertAssign
    840ms 4.11% 59.63% 4300ms 21.02% runtime.convT2E64
    710ms 3.47% 63.10% 2470ms 12.07% database/sql.(*Rows).Scan
    680ms 3.32% 66.42% 1260ms 6.16% runtime.deferreturn
    680ms 3.32% 69.75% 680ms 3.32% sync.(*RWMutex).RLock
    650ms 3.18% 72.92% 650ms 3.18% runtime.aeshash32
    630ms 3.08% 76.00% 630ms 3.08% sync.(*RWMutex).RUnlock
    Поделиться публикацией

    Комментарии 30

      0

      Не пробовали https://github.com/lazada/sqle?
      Ее под большую нагрузку и выборки делали.

        0
        Обязательно проверю, как найду время. По описанию — больше рассчитано на красоту Scan в структуры, без перечисления полей. Но и про оверхед минимальный пишут. Но как показывает практика, если завязываться с Reflect — то ты ничего не контролируешь.
          0
          Про Reflect не понял. Есть какой-то опыт, что получалось «ничего не контролируешь»?
          У меня пока получалось, что решения с Reflect получались только на несколько процентов медленнее аналогичного решения с кодогенерацией, если стараться этот самый Reflect кэшировать.
            0
            Зависит от задачи всё. В моем примере — reflect был скрыт под капотом драйвера. И, возможно, на выборке SELECT 20-ти записей — его оверхед не заметен вообще никак на фоне миллисекунд выполнения запроса.
            Но кешировать Reflect — вполне здравая идея. Это уже относится к задаче оптимизации работы с ним, которая появляется от того, что всё-таки там есть что ускорять. Я сам его использовал раз в месте которое выполнялось один раз при инициализации. Там можно не задумываться об оптимизациях. Но вот если в цикле на миллион шагов — тут уже я бы подумал про его применение.
          0
          Протестировал. Отписал в статье результат. Если кратко, то пилюли, которая сразу всем и всё улучшает не получилось, как я и предполагал по описанию из их README.
            0
            Благодарю!
              +1
              и не должно было получиться
              sqle это про удобство а не про производительность
              0
              Работал в этой компании. Нагрузки действительно большие.
              Но не думаю что именно эта библиоетека хоть как то решает проблемы производительности
                0

                Доброго дня, коллега!
                В product service нам она дала разницы до 8x по сравнению со стандартной.

                  0
                  JekaMas не поделитесь в двух словах в каких случаях/запросах она дает выиграш?
                    0
                    У нас были большие структуры со множеством вложений друг в друга, в каждой структуре могло быть 100 и более полей различного типа, который заранее, до выполнения запроса, знать было невозможно. Базы были на десятки миллионов записей и более.
                      0
                      предположу что разница в 8 раз была из-за неоптимального собственного кода
                        0
                        Это не так. Задача писать библиотеку родилась после того, как за два года исчерпали другие возможности.
              0
              Вроде бы статья должна называться «ускоряем Go драйвер под MySQL»?
                0
                Вот драйвер я никак не ускорял, признаюсь честно. Исследовал внутренности, пробовал разные протоколы и типы данных — это да. Цель была проста: получить как можно более быструю скорость обработки SELECT c целочисленными полями. Её и достигал.
                0

                Я Go знаю плохо, но данная строка меня сильно смущает конкатенацией: db.Query("SELECT siteID, zoneID, poolID, mediaID, campaignID FROM "+where).

                  0
                  Это да, совсем не по канонам.
                  Однако, как я понял, эту программу использует только автор в личных целях, по этому иньекций можно не бояться
                    0
                    Скрипт запускается по крону раз в сутки через докер. Инъекций там быть не должно.
                    +1
                    Мне кажется, что всё можно сделать на чистом SQL, используя count, sum и group by
                      0
                      Естественно, можно и таким путем пойти.
                      Для примера я оставил только 5 объектов. Их на самом деле больше. Есть ещё связки типа баннер-кампания. Есть связки через дополнительные поля, которые в других таблицах. В итоге если делать 10 запросов GROUP BY подряд с INSERT'ом в таблицы статистики — мы нагрузим MySQL, залочим таблицы. Плюс есть ещё особая логика подсчета дополнительных параметров — я её выбросил для чистоты експеримента. Ну и в планах делать антифрод по прошествии суток — там уже на SQL никак не провести анализ — нужны деревья.
                      Язык Go, как мне кажется, больше подходит для работы с большими наборами данных. Проблема в универсальных драйверах БД, которые стремясь угодить программисту, дают оверхед на конвертацию пришедшего из базы туда-сюда-обратно. Вот я и выяснил для себя какая связка дает большую скорость, далее, видимо, закодирую её в каких-то функциях, чтобы перевести остальную часть проекта на более производительные SELECT-ы. Ну и решил поделиться с сообществом, ибо такие задачи для Go должны возникать в любом серьезном проекте.
                        0

                        Тупиковый путь, имхо. Забирать все данные из базы, что бы потом обработать их на Go. И пересчет статистики ночью — тоже. Часто статистика нужна в реальном времени (с задержкой в пять минут, а не ночь). Хорошо, если вам пока хватает раз в сутки.

                          0
                          Я же не описывал весь движок — статья не про это. Статистика за сегодня из сервера сбрасывается раз в минуту в базу. Плюс фронт-енд может запросить напрямую у сервера json с актуальными полями по любому объекту. Так что у нас полный риалтайм касаемо сегодняшних данных.
                          Но допустим вам надо вычесть из статистики фрод-показы и актуализировать вчерашние данные. Как тогда быть?
                            0

                            Для сложной логики вам виднее. Если без получения полных данных никак, значит никак. Но и здесь направление "раз в сутки" выглядит не очень хорошо — ведь сутки у всех заканчиваются в разное время. То, что для вас — ночь, для других — разгар дня.


                            И поставлю +1 за кликхаус, будет интересно почитать, если напишите результаты вашего тестирования.

                          0
                          В итоге если делать 10 запросов GROUP BY подряд с INSERT'ом в таблицы статистики — мы нагрузим MySQL, залочим таблицы.

                          Зачем? Просто выбрать с GROUP BY, а дальше то же самое, просто не someCounter.Inc(someId), а someCounter.Add(someId, someCount).


                          И скорость выборки 36 секунд для почти 56 миллионов записей.

                          56 миллионов записей, по 5 4-байтовых интов в каждой, 56M*20 = 1120M, больше гигабайта. Мне кажется, основное время уходит на передачу данных, а конвертация занимает копейки.

                            0
                            Если делать GROUP BY campaignID — как это поможет посчитать кол-во по bannerID или siteID? Делать потом ещё 10 запросов?
                            Плюс GROUP BY на MySQL не такой шустрый, например
                            SELECT campaignID, COUNT(*) FROM hit_20180507 GROUP BY campaignID
                            Занимает 8,6 секунд на этом сервере на прогретых данных. А группировка по двум полям уже 12,9 секунд.

                            36 секунд я путем манипуляций превратил в 27 секунд. И судя по профилировщику — там можно выжать ещё пару секунд, убрав ненужные конвертации через строку. Данные находятся на localhost-сервере, 5 Гб на диске SSD, ещё и в кеше. Думаю сама передача занимает секунд 10.
                            Проверил — оставил цикл
                            for rows.Next() {
                            c++
                            }
                            итог — 11.9 секунд. Остальное время тратится на доступ к хеш-мапам, ну и конечно же на Scan.
                              0
                              36 секунд я путем манипуляций превратил в 27 секунд.

                              9 секунд на 56 миллионов итераций. Если итераций будет меньше, то и экономия будет не такая заметная. Но согласен, я слишком преувеличил.


                              Делать потом ещё 10 запросов?

                              Да. Можно сделать один с GROUP BY по всем параметрам, остальное в приложении досчитать. Счет строк будет на тысячи, а не на миллионы. Но судя по цифрам это не подходит. Хотя все равно многовато как-то.

                                0
                                Go как раз предназначен для обработки миллионов строк. Не надо стесняться его применять для этого. За 26 секунд обработать результат работы целых суток — по-моему хорошее соотношение, которое оставляет задел на стократный рост. Хотя на С++ думаю в 2 раза быстрей было бы по скорости обработки. Но есть ещё такой параметр как время на разработку, ту же компиляцию, отладку и поддержку. И тут Go для меня выигрывает. Плюс приятно в проекте иметь один язык — и для сервера и для скриптов обработки.
                                Если бы я писал обработку этого запроса на PHP, к примеру, я бы тоже постарался уменьшить число строк с помощью БД. Там цикл на сто тысяч уже может показаться «вечным» и оптимизировать что-то как здесь — не представляется даже возможным.
                                  +1

                                  В некоторых случаях после group by количество строк будет примерно равно количеству до (может в разы сократится, но не в десять). Рекламная сеть, как раз такой случай там каждый второй клик может быть уникален с точки зрения набора группировочных параметров.

                          0

                          А если просто взять clickhouse? Я использую для мгновенных выборок из сотен миллионов даных. Можно еще использовать ProxySQL https://github.com/sysown/proxysql/wiki/ClickHouse-Support

                            0
                            Кликхаус будет следующий объект для тестов. Сейчас вставляю параллельно в 2 базы — пока длится процесс перехода. А какой библиотекой вы вычитываете в Go миллионы строк?

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое