الانتقال إلى المحتوى الرئيسي
تتوفر جميع أمثلة الشيفرة الخاصة بـ ClickHouse API هنا. لإعداد الاتصال، راجع التهيئة. للاطلاع على أنواع البيانات المدعومة وتعيينات الأنواع في Go، راجع أنواع البيانات.

الاتصال

يوضح المثال التالي، الذي يعرض إصدار الخادم، كيفية الاتصال بـ ClickHouse، على افتراض أن ClickHouse غير مؤمَّن ويمكن الوصول إليه باستخدام المستخدم default. لاحظ أننا نستخدم منفذ native الافتراضي للاتصال.
conn, err := clickhouse.Open(&clickhouse.Options{
    Addr: []string{fmt.Sprintf("%s:%d", env.Host, env.Port)},
    Auth: clickhouse.Auth{
        Database: env.Database,
        Username: env.Username,
        Password: env.Password,
    },
})
if err != nil {
    return err
}
v, err := conn.ServerVersion()
fmt.Println(v)
مثال كامل في جميع الأمثلة التالية، ما لم يُذكر خلاف ذلك صراحةً، نفترض أن المتغير conn الخاص بـ ClickHouse قد تم إنشاؤه وهو متاح.

التنفيذ

يمكن تنفيذ عبارات مختلفة عبر الطريقة Exec. وهذا مفيد لعبارات DDL والعبارات البسيطة. ولا ينبغي استخدامها لعمليات الإدراج الكبيرة أو للتكرار على نتائج الاستعلامات.
conn.Exec(context.Background(), `DROP TABLE IF EXISTS example`)
err = conn.Exec(context.Background(), `
    CREATE TABLE IF NOT EXISTS example (
        Col1 UInt8,
        Col2 String
    ) engine=Memory
`)
if err != nil {
    return err
}
conn.Exec(context.Background(), "INSERT INTO example VALUES (1, 'test-1')")
مثال كامل لاحظ أنه يمكن تمرير Context إلى الاستعلام. ويمكن استخدام ذلك لتمرير إعدادات محددة على مستوى الاستعلام — راجع استخدام Context.

الإدراج على دفعات

لإدراج عدد كبير من الصفوف، يوفّر العميل آلية العمل على دفعات. ويتطلب ذلك تجهيز دفعة يمكن إلحاق الصفوف بها، ثم إرسالها في النهاية باستخدام الأسلوب Send(). وتبقى الدفعات في الذاكرة إلى أن يُنفَّذ Send. يُوصى باستدعاء Close على الدفعة لتجنّب تسرّب الاتصالات. ويمكن القيام بذلك باستخدام الكلمة المحجوزة defer بعد تجهيز الدفعة. وسيؤدي ذلك إلى تنظيف الاتصال إذا لم يُستدعَ Send مطلقًا. لاحظ أن هذا سيؤدي إلى ظهور 0 من عمليات إدراج الصفوف في سجل الاستعلامات إذا لم يتم إلحاق أي صفوف.
conn, err := GetNativeConnection(nil, nil, nil)
if err != nil {
    return err
}
ctx := context.Background()
defer func() {
    conn.Exec(ctx, "DROP TABLE example")
}()
conn.Exec(context.Background(), "DROP TABLE IF EXISTS example")
err = conn.Exec(ctx, `
    CREATE TABLE IF NOT EXISTS example (
            Col1 UInt8
        , Col2 String
        , Col3 FixedString(3)
        , Col4 UUID
        , Col5 Map(String, UInt8)
        , Col6 Array(String)
        , Col7 Tuple(String, UInt8, Array(Map(String, String)))
        , Col8 DateTime
    ) Engine = Memory
`)
if err != nil {
    return err
}

batch, err := conn.PrepareBatch(ctx, "INSERT INTO example")
if err != nil {
    return err
}
defer batch.Close()

for i := 0; i < 1000; i++ {
    err := batch.Append(
        uint8(42),
        "ClickHouse",
        "Inc",
        uuid.New(),
        map[string]uint8{"key": 1},             // Map(String, UInt8)
        []string{"Q", "W", "E", "R", "T", "Y"}, // Array(String)
        []interface{}{ // Tuple(String, UInt8, Array(Map(String, String)))
            "String Value", uint8(5), []map[string]string{
                {"key": "value"},
                {"key": "value"},
                {"key": "value"},
            },
        },
        time.Now(),
    )
    if err != nil {
        return err
    }
}

return batch.Send()
مثال كامل تنطبق توصيات ClickHouse هنا. لا ينبغي مشاركة الدُفعات بين go-routines — أنشئ دفعة منفصلة لكل روتين. استنادًا إلى المثال أعلاه، لاحظ ضرورة توافق أنواع المتغيرات مع نوع العمود عند إضافة الصفوف. ومع أن المطابقة تكون واضحة عادةً، فإن هذه الواجهة تحاول أن تكون مرنة، وستُحوَّل الأنواع ما لم يترتب على ذلك فقدان في الدقة. على سبيل المثال، يوضح ما يلي إدراج قيمة نصية في datetime64.
batch, err := conn.PrepareBatch(ctx, "INSERT INTO example")
if err != nil {
    return err
}
defer batch.Close()

for i := 0; i < 1000; i++ {
    err := batch.Append(
        "2006-01-02 15:04:05.999",
    )
    if err != nil {
        return err
    }
}

return batch.Send()
مثال كامل للاطّلاع على ملخص كامل لأنواع Go المدعومة لكل نوع من الأعمدة، راجع تحويلات الأنواع.

الأعمدة المؤقتة

الأعمدة المؤقتة هي أعمدة للكتابة فقط، ولا توجد إلا أثناء الإدراج — فهي لا تُخزَّن ولا يمكن الاستعلام عنها. وهي مفيدة لحساب قيم الأعمدة المشتقة وقت الإدراج.
ctx := context.Background()
ddl := `
CREATE OR REPLACE TABLE test
(
    id UInt64,
    unhexed String EPHEMERAL,
    hexed FixedString(4) DEFAULT unhex(unhexed)
)
ENGINE = MergeTree
ORDER BY id`

if err := conn.Exec(ctx, ddl); err != nil {
    return err
}

// Insert by providing the ephemeral column value
if err := conn.Exec(ctx, "INSERT INTO test (id, unhexed) VALUES (1, '5a90b714')"); err != nil {
    return err
}

// Only non-ephemeral columns can be queried
rows, err := conn.Query(ctx, "SELECT id, hexed, hex(hexed) FROM test")
مثال كامل

الاستعلام عن الصفوف

يمكنك إما الاستعلام عن صف واحد باستخدام الدالة QueryRow، أو الحصول على مؤشر للتكرار عبر مجموعة النتائج باستخدام Query. وبينما تتيح الأولى تمرير وجهة تُسلسَل البيانات إليها، تتطلب الثانية استدعاء Scan لكل صف.
row := conn.QueryRow(context.Background(), "SELECT * FROM example")
var (
    col1             uint8
    col2, col3, col4 string
    col5             map[string]uint8
    col6             []string
    col7             []interface{}
    col8             time.Time
)
if err := row.Scan(&col1, &col2, &col3, &col4, &col5, &col6, &col7, &col8); err != nil {
    return err
}
fmt.Printf("row: col1=%d, col2=%s, col3=%s, col4=%s, col5=%v, col6=%v, col7=%v, col8=%v\n", col1, col2, col3, col4, col5, col6, col7, col8)
المثال الكامل
rows, err := conn.Query(ctx, "SELECT Col1, Col2, Col3 FROM example WHERE Col1 >= 2")
if err != nil {
    return err
}
for rows.Next() {
    var (
        col1 uint8
        col2 string
        col3 time.Time
    )
    if err := rows.Scan(&col1, &col2, &col3); err != nil {
        return err
    }
    fmt.Printf("row: col1=%d, col2=%s, col3=%s\n", col1, col2, col3)
}
rows.Close()
return rows.Err()
مثال كامل لاحظ أنه في كلتا الحالتين، يجب تمرير مؤشر إلى المتغيرات التي نريد إسناد قيم الأعمدة المقابلة إليها. ويجب تمرير هذه المؤشرات بالترتيب المحدد في عبارة SELECT — وبشكل افتراضي، سيُستخدم ترتيب تعريف الأعمدة عند استخدام SELECT * كما هو موضح أعلاه. وعلى غرار الإدراج، تتطلب طريقة Scan أن تكون المتغيرات الهدف من نوع مناسب. والهدف هنا أيضًا هو توفير المرونة، مع تحويل الأنواع حيثما أمكن، ما دام ذلك لا يؤدي إلى فقدان في الدقة. على سبيل المثال، يوضح المثال أعلاه قراءة عمود UUID إلى متغير string. للحصول على قائمة كاملة بأنواع Go المدعومة لكل نوع عمود، راجع تحويلات الأنواع. أخيرًا، لاحظ إمكانية تمرير Context إلى الطريقتين Query وQueryRow. ويمكن استخدام ذلك لإعدادات على مستوى الاستعلام — راجع استخدام Context لمزيد من التفاصيل.

الإدراج غير المتزامن

تدعم عمليات الإدراج غير المتزامنة عبر الطريقة Async. ويتيح ذلك للمستخدم تحديد ما إذا كان ينبغي للعميل انتظار الخادم حتى يُكمل عملية الإدراج، أو أن يستجيب بمجرد استلام البيانات. وهذا يتحكم فعليًا في المعلَمة wait_for_async_insert.
conn, err := GetNativeConnection(nil, nil, nil)
if err != nil {
    return err
}
ctx := context.Background()
if err := clickhouse_tests.CheckMinServerServerVersion(conn, 21, 12, 0); err != nil {
    return nil
}
defer func() {
    conn.Exec(ctx, "DROP TABLE example")
}()
conn.Exec(ctx, `DROP TABLE IF EXISTS example`)
const ddl = `
    CREATE TABLE example (
            Col1 UInt64
        , Col2 String
        , Col3 Array(UInt8)
        , Col4 DateTime
    ) ENGINE = Memory
`
if err := conn.Exec(ctx, ddl); err != nil {
    return err
}
for i := 0; i < 100; i++ {
    if err := conn.AsyncInsert(ctx, fmt.Sprintf(`INSERT INTO example VALUES (
        %d, '%s', [1, 2, 3, 4, 5, 6, 7, 8, 9], now()
    )`, i, "Golang SQL database driver"), false); err != nil {
        return err
    }
}
المثال الكامل

الإدراج بصيغة الأعمدة

يمكن تنفيذ عمليات الإدراج بصيغة الأعمدة. وقد يوفّر ذلك مزايا في الأداء إذا كانت البيانات منظَّمة مسبقًا بهذه البنية، لأنه يجنّب الحاجة إلى تحويلها إلى صفوف.
batch, err := conn.PrepareBatch(context.Background(), "INSERT INTO example")
if err != nil {
    return err
}
defer batch.Close()

var (
    col1 []uint64
    col2 []string
    col3 [][]uint8
    col4 []time.Time
)
for i := 0; i < 1_000; i++ {
    col1 = append(col1, uint64(i))
    col2 = append(col2, "Golang SQL database driver")
    col3 = append(col3, []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9})
    col4 = append(col4, time.Now())
}
if err := batch.Column(0).Append(col1); err != nil {
    return err
}
if err := batch.Column(1).Append(col2); err != nil {
    return err
}
if err := batch.Column(2).Append(col3); err != nil {
    return err
}
if err := batch.Column(3).Append(col4); err != nil {
    return err
}

return batch.Send()
مثال كامل

استخدام structs

توفّر structs في Golang للمستخدمين تمثيلًا منطقيًا لصف من البيانات في ClickHouse. ولتسهيل ذلك، توفّر الواجهة الأصلية عدة دوال ملائمة.

Select مع serialize

تتيح الطريقة Select تحويل مجموعة من صفوف الاستجابة إلى slice من structs عبر استدعاء واحد.
var result []struct {
    Col1           uint8
    Col2           string
    ColumnWithName time.Time `ch:"Col3"`
}

if err = conn.Select(ctx, &result, "SELECT Col1, Col2, Col3 FROM example"); err != nil {
    return err
}

for _, v := range result {
    fmt.Printf("row: col1=%d, col2=%s, col3=%s\n", v.Col1, v.Col2, v.ColumnWithName)
}
مثال كامل

Scan struct

يتيح ScanStruct تحويل صف واحد من استعلام إلى struct.
var result struct {
    Col1  int64
    Count uint64 `ch:"count"`
}
if err := conn.QueryRow(context.Background(), "SELECT Col1, COUNT() AS count FROM example WHERE Col1 = 5 GROUP BY Col1").ScanStruct(&result); err != nil {
    return err
}
مثال كامل

إلحاق struct

يتيح AppendStruct إلحاق struct بـ دفعة موجودة وتفسيرها على أنها صف كامل. ويتطلب ذلك أن تتطابق أعمدة struct مع الجدول من حيث الاسم والنوع. وبينما يجب أن يكون لكل الأعمدة حقل مكافئ في struct، قد لا يكون لبعض حقول struct عمود مكافئ. وسيتم تجاهل هذه الحقول ببساطة.
batch, err := conn.PrepareBatch(context.Background(), "INSERT INTO example")
if err != nil {
    return err
}
defer batch.Close()

for i := 0; i < 1_000; i++ {
    err := batch.AppendStruct(&row{
        Col1:       uint64(i),
        Col2:       "Golang SQL database driver",
        Col3:       []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9},
        Col4:       time.Now(),
        ColIgnored: "this will be ignored",
    })
    if err != nil {
        return err
    }
}
مثال كامل

ربط المعلمات

يدعم العميل ربط المعلمات في الطرق Exec وQuery وQueryRow. وكما هو موضح في المثال أدناه، يُدعَم ذلك باستخدام المعلمات المُسمّاة والمرقّمة والموضعية. ونورد أمثلة على ذلك أدناه.
var count uint64
// positional bind
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col1 >= ? AND Col3 < ?", 500, now.Add(time.Duration(750)*time.Second)).Scan(&count); err != nil {
    return err
}
// 250
fmt.Printf("Positional bind count: %d\n", count)
// numeric bind
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col1 <= $2 AND Col3 > $1", now.Add(time.Duration(150)*time.Second), 250).Scan(&count); err != nil {
    return err
}
// 100
fmt.Printf("Numeric bind count: %d\n", count)
// named bind
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col1 <= @col1 AND Col3 > @col3", clickhouse.Named("col1", 100), clickhouse.Named("col3", now.Add(time.Duration(50)*time.Second))).Scan(&count); err != nil {
    return err
}
// 50
fmt.Printf("Named bind count: %d\n", count)
المثال الكامل

حالات خاصة

بشكل افتراضي، ستُفكّك المقاطع إلى قائمة قيم مفصولة بفواصل إذا مُرِّرت كمُعامل إلى query. وإذا كنت بحاجة إلى إدراج مجموعة من القيم محاطةً بـ [ ]، فيجب استخدام ArraySet. إذا كانت المجموعات/tuples مطلوبة، ومحاطةً بـ ( )، على سبيل المثال لاستخدامها مع عوامل التشغيل IN، فيمكنك استخدام GroupSet. ويكون ذلك مفيدًا بشكل خاص عندما تكون هناك حاجة إلى مجموعات متعددة، كما هو موضح في المثال أدناه. أخيرًا، تتطلب حقول DateTime64 تحديد precision لضمان عرض المُعاملات بالشكل المناسب. لكن مستوى precision لهذا الحقل غير معروف لدى العميل، لذا يجب على المستخدم توفيره. ولتسهيل ذلك، نوفّر المُعامل DateNamed.
var count uint64
// arrays will be unfolded
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col1 IN (?)", []int{100, 200, 300, 400, 500}).Scan(&count); err != nil {
    return err
}
fmt.Printf("Array unfolded count: %d\n", count)
// arrays will be preserved with []
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col4 = ?", clickhouse.ArraySet{300, 301}).Scan(&count); err != nil {
    return err
}
fmt.Printf("Array count: %d\n", count)
// Group sets allow us to form ( ) lists
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col1 IN ?", clickhouse.GroupSet{[]interface{}{100, 200, 300, 400, 500}}).Scan(&count); err != nil {
    return err
}
fmt.Printf("Group count: %d\n", count)
// More useful when we need nesting
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE (Col1, Col5) IN (?)", []clickhouse.GroupSet{{[]interface{}{100, 101}}, {[]interface{}{200, 201}}}).Scan(&count); err != nil {
    return err
}
fmt.Printf("Group count: %d\n", count)
// Use DateNamed when you need a precision in your time#
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col3 >= @col3", clickhouse.DateNamed("col3", now.Add(time.Duration(500)*time.Millisecond), clickhouse.NanoSeconds)).Scan(&count); err != nil {
    return err
}
fmt.Printf("NamedDate count: %d\n", count)
المثال الكامل

استخدام السياق

توفّر سياقات Go وسيلةً لتمرير المهل الزمنية، وإشارات الإلغاء، وغيرها من القيم المرتبطة بنطاق الطلب عبر حدود واجهة برمجة تطبيقات. تقبل جميع الطرائق على الاتصال سياقًا بوصفه المعامل الأول. وبينما استخدمت الأمثلة السابقة context.Background()، يمكنك استخدام هذه الإمكانية لتمرير الإعدادات والمهل الزمنية وإلغاء الاستعلامات. يتيح تمرير سياق أُنشئ باستخدام withDeadline فرض حدود على وقت تنفيذ الاستعلامات. لاحظ أن هذا وقتٌ مطلق، وأن انتهاء المهلة لن يؤدي إلا إلى تحرير الاتصال وإرسال إشارة إلغاء إلى ClickHouse. ويمكن استخدام WithCancel بدلًا من ذلك لإلغاء استعلام صراحةً. تتيح الأداتان المساعدتان clickhouse.WithQueryID و clickhouse.WithQuotaKey تحديد معرّف استعلام ومفتاح حصة. ويمكن أن تكون معرّفات الاستعلامات مفيدة لتتبّع الاستعلامات في السجلات ولأغراض الإلغاء. كما يمكن استخدام مفتاح الحصة لفرض حدود على استخدام ClickHouse استنادًا إلى قيمة مفتاح فريدة — راجع إدارة الحصص لمزيد من التفاصيل. يمكنك أيضًا استخدام السياق لضمان تطبيق إعداد معيّن على استعلام محدد فقط، بدلًا من تطبيقه على الاتصال بالكامل، كما هو موضح في إعدادات الاتصال. أخيرًا، يمكنك التحكم في حجم المخزن المؤقت للكتل عبر clickhouse.WithBlockSize. يتجاوز هذا الإعدادَ على مستوى الاتصال BlockBufferSize، ويتحكم في الحد الأقصى لعدد الكتل التي تُفك شيفرتها ويُحتفظ بها في الذاكرة في أي وقت. وقد تعني القيم الأكبر قدرًا أكبر من التوازي على حساب الذاكرة. تظهر أمثلة على ما سبق أدناه.
dialCount := 0
conn, err := clickhouse.Open(&clickhouse.Options{
    Addr: []string{fmt.Sprintf("%s:%d", env.Host, env.Port)},
    Auth: clickhouse.Auth{
        Database: env.Database,
        Username: env.Username,
        Password: env.Password,
    },
    DialContext: func(ctx context.Context, addr string) (net.Conn, error) {
        dialCount++
        var d net.Dialer
        return d.DialContext(ctx, "tcp", addr)
    },
})
if err != nil {
    return err
}
if err := clickhouse_tests.CheckMinServerServerVersion(conn, 22, 6, 1); err != nil {
    return nil
}
// we can use context to pass settings to a specific API call
ctx := clickhouse.Context(context.Background(), clickhouse.WithSettings(clickhouse.Settings{
    "async_insert": "1",
}))

// queries can be cancelled using the context
ctx, cancel := context.WithCancel(context.Background())
go func() {
    cancel()
}()
if err = conn.QueryRow(ctx, "SELECT sleep(3)").Scan(); err == nil {
    return fmt.Errorf("expected cancel")
}

// set a deadline for a query - this will cancel the query after the absolute time is reached.
// queries will continue to completion in ClickHouse
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
defer cancel()
if err := conn.Ping(ctx); err == nil {
    return fmt.Errorf("expected deadline exceeeded")
}

// set a query id to assist tracing queries in logs e.g. see system.query_log
var one uint8
queryId, _ := uuid.NewUUID()
ctx = clickhouse.Context(context.Background(), clickhouse.WithQueryID(queryId.String()))
if err = conn.QueryRow(ctx, "SELECT 1").Scan(&one); err != nil {
    return err
}

conn.Exec(context.Background(), "DROP QUOTA IF EXISTS foobar")
defer func() {
    conn.Exec(context.Background(), "DROP QUOTA IF EXISTS foobar")
}()
ctx = clickhouse.Context(context.Background(), clickhouse.WithQuotaKey("abcde"))
// set a quota key - first create the quota
if err = conn.Exec(ctx, "CREATE QUOTA IF NOT EXISTS foobar KEYED BY client_key FOR INTERVAL 1 minute MAX queries = 5 TO default"); err != nil {
    return err
}

type Number struct {
    Number uint64 `ch:"number"`
}
for i := 1; i <= 6; i++ {
    var result []Number
    if err = conn.Select(ctx, &result, "SELECT number FROM numbers(10)"); err != nil {
        return err
    }
}
المثال الكامل

معلومات Progress وProfile وLog

يمكن طلب معلومات Progress وProfile وLog مع الاستعلامات. تعرض معلومات Progress إحصاءات عن عدد الصفوف والبايتات التي تمت قراءتها ومعالجتها في ClickHouse. في المقابل، توفّر معلومات Profile ملخصًا للبيانات المُعادة إلى العميل، بما في ذلك إجماليات البايتات (غير المضغوطة) والصفوف والكتل. وأخيرًا، توفّر معلومات Log إحصاءات عن سلاسل التنفيذ، مثل استخدام الذاكرة وسرعة البيانات. يتطلب الحصول على هذه المعلومات من المستخدم استخدام Context، حيث يمكنه تمرير دوال استدعاء إليه.
totalRows := uint64(0)
// use context to pass a call back for progress and profile info
ctx := clickhouse.Context(context.Background(), clickhouse.WithProgress(func(p *clickhouse.Progress) {
    fmt.Println("progress: ", p)
    totalRows += p.Rows
}), clickhouse.WithProfileInfo(func(p *clickhouse.ProfileInfo) {
    fmt.Println("profile info: ", p)
}), clickhouse.WithLogs(func(log *clickhouse.Log) {
    fmt.Println("log info: ", log)
}))

rows, err := conn.Query(ctx, "SELECT number from numbers(1000000) LIMIT 1000000")
if err != nil {
    return err
}
for rows.Next() {
}

// NOTE: Do not skip rows.Err() check
if err := rows.Err(); err != nil {
    return err
}

fmt.Printf("Total Rows: %d\n", totalRows)
rows.Close()
المثال الكامل

المسح الديناميكي

قد تحتاج إلى قراءة جداول لا تعرف مخططها أو أنواع الحقول التي تُرجعها. ويشيع هذا في الحالات التي يُجرى فيها تحليل بيانات مخصّص أو تُطوَّر فيها أدوات عامة. ولتحقيق ذلك، تتوفر معلومات أنواع الأعمدة ضمن استجابات الاستعلام. ويمكن استخدام ذلك مع reflection في Go لإنشاء مثيلات وقت التشغيل لمتغيرات ذات أنواع صحيحة، بحيث يمكن تمريرها إلى Scan.
const query = `
SELECT
        1     AS Col1
    , 'Text' AS Col2
`
rows, err := conn.Query(context.Background(), query)
if err != nil {
    return err
}
defer rows.Close()
var (
    columnTypes = rows.ColumnTypes()
    vars        = make([]interface{}, len(columnTypes))
)
for i := range columnTypes {
    vars[i] = reflect.New(columnTypes[i].ScanType()).Interface()
}
for rows.Next() {
    if err := rows.Scan(vars...); err != nil {
        return err
    }
    for _, v := range vars {
        switch v := v.(type) {
        case *string:
            fmt.Println(*v)
        case *uint8:
            fmt.Println(*v)
        }
    }
}
// NOTE: Do not skip rows.Err() check
if err := rows.Err(); err != nil {
    return err
}
المثال الكامل

الجداول الخارجية

تسمح الجداول الخارجية للعميل بإرسال بيانات إلى ClickHouse مع استعلام SELECT. وتُوضَع هذه البيانات في جدول مؤقت، ويمكن استخدامها داخل الاستعلام نفسه أثناء تنفيذ الاستعلام. لإرسال بيانات خارجية مع استعلام عبر العميل، يجب على المستخدم إنشاء جدول خارجي باستخدام ext.NewTable قبل تمريره عبر السياق.
table1, err := ext.NewTable("external_table_1",
    ext.Column("col1", "UInt8"),
    ext.Column("col2", "String"),
    ext.Column("col3", "DateTime"),
)
if err != nil {
    return err
}

for i := 0; i < 10; i++ {
    if err = table1.Append(uint8(i), fmt.Sprintf("value_%d", i), time.Now()); err != nil {
        return err
    }
}

table2, err := ext.NewTable("external_table_2",
    ext.Column("col1", "UInt8"),
    ext.Column("col2", "String"),
    ext.Column("col3", "DateTime"),
)

for i := 0; i < 10; i++ {
    table2.Append(uint8(i), fmt.Sprintf("value_%d", i), time.Now())
}
ctx := clickhouse.Context(context.Background(),
    clickhouse.WithExternalTable(table1, table2),
)
rows, err := conn.Query(ctx, "SELECT * FROM external_table_1")
if err != nil {
    return err
}
for rows.Next() {
    var (
        col1 uint8
        col2 string
        col3 time.Time
    )
    rows.Scan(&col1, &col2, &col3)
    fmt.Printf("col1=%d, col2=%s, col3=%v\n", col1, col2, col3)
}
// NOTE: Do not skip rows.Err() check
if err := rows.Err(); err != nil {
    return err
}
rows.Close()

var count uint64
if err := conn.QueryRow(ctx, "SELECT COUNT(*) FROM external_table_1").Scan(&count); err != nil {
    return err
}
fmt.Printf("external_table_1: %d\n", count)
if err := conn.QueryRow(ctx, "SELECT COUNT(*) FROM external_table_2").Scan(&count); err != nil {
    return err
}
fmt.Printf("external_table_2: %d\n", count)
if err := conn.QueryRow(ctx, "SELECT COUNT(*) FROM (SELECT * FROM external_table_1 UNION ALL SELECT * FROM external_table_2)").Scan(&count); err != nil {
    return err
}
fmt.Printf("external_table_1 UNION external_table_2: %d\n", count)
مثال كامل

OpenTelemetry

يدعم ClickHouse تمرير سياق التتبّع عبر بروتوكولي النقل TCP وHTTP. عند استخدام TCP، يقوم العميل بتسلسل span ضمن البروتوكول الثنائي الأصلي. استخدم clickhouse.WithSpan لإرفاق span باستعلام عبر الـ context.
قيد في نقل HTTPرغم أن ClickHouse server يقبل ترويسات HTTP القياسية traceparent / tracestate، فإن ناقل HTTP في clickhouse-go لا يرسلها حاليًا، لذا لا يكون لـ WithSpan أي تأثير عبر HTTP. وكحل بديل، يمكنك ضبط الترويسة يدويًا عبر HttpHeaders في خيارات الاتصال.
var count uint64
rows := conn.QueryRow(clickhouse.Context(context.Background(), clickhouse.WithSpan(
    trace.NewSpanContext(trace.SpanContextConfig{
        SpanID:  trace.SpanID{1, 2, 3, 4, 5},
        TraceID: trace.TraceID{5, 4, 3, 2, 1},
    }),
)), "SELECT COUNT() FROM (SELECT number FROM system.numbers LIMIT 5)")
if err := rows.Scan(&count); err != nil {
    return err
}
// NOTE: Do not skip rows.Err() check
if err := rows.Err(); err != nil {
    return err
}
fmt.Printf("count: %d\n", count)
مثال كامل يمكن العثور على التفاصيل الكاملة حول الاستفادة من إمكانات التتبّع ضمن دعم OpenTelemetry.
آخر تعديل في ٢٩ يونيو ٢٠٢٦