Octonica.ClickHouseClient

repository·master·Indexed 41 days ago

https://github.com/octonica/clickhouseclient

A high-performance .NET Core driver for ClickHouse that implements the ADO.NET DbProvider API. It supports the binary protocol, compression, and various ClickHouse data types. The library features a specialized ClickHouseColumnWriter for efficient bulk columnar inserts, OpenTelemetry tracing support, and the ability to pass client-defined tables to queries via IClickHouseTableProvider.

Tokens
4.9K
Snippets
12
Records
17
Agent score
51%

What's inside Octonica.ClickHouseClient

  1. What is the ClickHouseColumnWriter API?

    master
    The ClickHouseColumnWriter is an extended, non-ADO.NET compliant API designed for efficient data manipulation. While standard ADO.NET is useful for general queries, the ClickHouseColumnWriter is optimized for bulk loading data into ClickHouse by leveraging its columnar architecture. You can obtain a writer by calling ClickHouseConnection.CreateColumnWriter().
  2. Supported data types for columns

    master

    When using WriteTable or WriteTableAsync, each column can be any type that implements one of the following interfaces:

    • IReadOnlyList<T>
    • IList<T>
    • IEnumerable<T>
    • IAsyncEnumerable<T> (Supported only by WriteTableAsync)
    • IEnumerable (non-generic)

    Constraints:

    • The number of columns provided must match the number of columns in the initial INSERT query.
    • Columns may have different numbers of elements, but they must contain at least rowCount elements.
    • Columns must be passed in the order defined by the query (unless using the dictionary-based overload).
  3. Understand ClickHouse to .NET type mappings

    master

    When reading data from ClickHouse using Octonica.ClickHouseClient.ClickHouseDataReader, the driver maps ClickHouse column types to specific .NET types.

    • Default Type: The type returned by GetFieldType(int ordinal).
    • GetValue(int ordinal): Returns either the default type or System.DBNull.
    • GetFieldValue<T>(int ordinal): Allows you to explicitly request a supported type.
    • GetFieldValue<T>(int ordinal, T? nullValue): A safer way to retrieve values that avoids throwing errors when encountering NULL values by providing a fallback nullValue.

    Note: For Date and Date32 types, System.DateOnly is the default type in .NET 6.0 and later. In older .NET versions, these map to System.DateTime.

    // Example of retrieving a value with a fallback for NULLs
    var value = reader.GetFieldValue<int>(ordinal, -1);
  4. Pass client-defined tables to queries

    master

    ClickHouse allows passing arbitrary tables along with a query. In ClickHouseClient, these are managed via the TableProviders collection on a ClickHouseCommand.

    To implement a custom table, implement the IClickHouseTableProvider interface. The library provides a default implementation, ClickHouseTableProvider, which allows you to build tables in a columnar format similar to ClickHouseColumnWriter.

    How to use ClickHouseTableProvider:

    1. Instantiate ClickHouseTableProvider with a table name and the expected row count.
    2. Use .AddColumn(name, data) to add columns. Column settings (like ClickHouseDbType or IsNullable) can be configured on the returned column object.
    3. Add the provider to cmd.TableProviders.
    4. Reference the table name directly in your SQL query without special syntax.
    using var connection = new ClickHouseConnection(connectionStr);
    connection.Open();
    
    var cmd = connection.CreateCommand("SELECT ptable.id, ptable.user, ptable.ip FROM ptable");
    
    var users = new[] {"user1", "user2", "admin1", "admin2"};
    var ips = new[] {"1.1.1.1", "2.2.2.2", "127.0.0.1", "::ffff:192.0.2.1"};
    
    var pTableProvider = new ClickHouseTableProvider("ptable", users.Length);
    pTableProvider.AddColumn("id", Enumerable.Range(1, users.Length));
    pTableProvider.AddColumn("user", users);
    
    // Configure column settings
    var ipColumn = pTableProvider.AddColumn("ip", ips);
    ipColumn.ClickHouseDbType = ClickHouseDbType.IpV6;
    ipColumn.IsNullable = false;
    
    cmd.TableProviders.Add(pTableProvider);
    
    using var reader = cmd.ExecuteReader();
    while(reader.Read())
    {
      // Reading the data
    }
  5. Use ClickHouseColumnWriter for high-performance bulk inserts

    master

    The ClickHouseColumnWriter class is designed for writing large amounts of data to ClickHouse tables using a columnar layout. Instead of inserting row-by-row, you provide entire columns of data at once. This is significantly more efficient for large datasets.

    To use it, create a writer from a ClickHouseConnection using CreateColumnWriter or CreateColumnWriterAsync.

    Important Requirement: The INSERT query provided to the writer must end with the VALUES keyword, but must not include the actual list of values (e.g., INSERT INTO table_name VALUES).

    using var connection = new ClickHouseConnection(connectionStr);
    connection.Open();
    
    // The query must end with 'VALUES' and no actual data
    using var writer = connection.CreateColumnWriter("INSERT INTO some_table VALUES");
  6. Configure ClickHouse connection strings

    master

    Connections can be established using a standard connection string or the ClickHouseConnectionStringBuilder class.

    Connection String Syntax: Host=<host>;Port=<port>;Database=<db>;Password=<pass>

    Example: "Host=127.0.0.1;Password=P@ssw0rd; Database=db"

    var sb = new ClickHouseConnectionStringBuilder();
    sb.Host = "127.0.0.1";
    // ... set other properties
    using var conn = new ClickHouseConnection(sb);
  7. Use a temporary table with an IN clause

    master

    You can use a ClickHouseTableProvider to efficiently pass a set of values for use in an IN clause in your query.

    using var connection = new ClickHouseConnection(connectionStr);
    connection.Open();
    
    // Reference the table name 'param_table' directly in the IN clause
    var cmd = connection.CreateCommand("SELECT toInt32(number) FROM numbers(100000) WHERE number IN param_table");
    
    var tableProvider = new ClickHouseTableProvider("param_table", 100);
    tableProvider.AddColumn(Enumerable.Range(500, 100)); // Providing 100 values
    
    cmd.TableProviders.Add(tableProvider);
    
    using var reader = cmd.ExecuteReader();
    while(reader.Read())
    {
      // Reading only rows where number is in the provided range
    }
    using var connection = new ClickHouseConnection(connectionStr);
    connection.Open();
    
    var cmd = connection.CreateCommand("SELECT toInt32(number) FROM numbers(100000) WHERE number IN param_table");
    
    var tableProvider = new ClickHouseTableProvider("param_table", 100);
    tableProvider.AddColumn(Enumerable.Range(500, int.MaxValue / 2));
    
    cmd.TableProviders.Add(tableProvider);
    
    using var reader = cmd.ExecuteReader();
    while(reader.Read())
    {
      // Reading only 100 rows
    }
  8. Write data using named columns

    master

    If your INSERT query specifies column names (e.g., INSERT INTO table(col1, col2) VALUES), you can write data using an IReadOnlyDictionary<string, object?>. This allows you to map data to columns by their name rather than their position in an array.

    1. Create the writer with a query like INSERT INTO table(col1, col2) VALUES.
    2. Create a Dictionary<string, object?> where keys are the column names.
    3. Call WriteTable or WriteTableAsync passing the dictionary and the rowCount.
    avait using var connection = new ClickHouseConnection(connectionStr);
    await connection.OpenAsync();
    
    // Specify column names in the query
    await using var writer = connection.CreateColumnWriter("INSERT INTO some_table(id, dt, val) VALUES");
    
    var id = new List<Guid>();
    var dt = new List<DateTime>();
    var val = new List<decimal>();
    // ... fill lists ...
    
    var columns = new Dictionary<string, object?>
    {
    	["id"] = id,
    	["dt"] = dt,
    	["val"] = val
    };
    
    var rowCount = id.Count;
    await writer.WriteTableAsync(columns, rowCount, CancellationToken.None);
  9. Execute a simple asynchronous SELECT

    master

    The primary entry point for the API is the Octonica.ClickHouse.ClickHouseConnection class, which implements the standard ADO.NET DbConnection interface. You can execute queries asynchronously using CreateCommand and ExecuteScalarAsync.

    var sb = new ClickHouseConnectionStringBuilder();
    sb.Host = "127.0.0.1";
    using var conn = new ClickHouseConnection(sb);
    await conn.OpenAsync();
    var currentUser = await conn.CreateCommand("select currentUser()").ExecuteScalarAsync();
  10. Perform bulk inserts with ClickHouseColumnWriter

    master

    For high-performance bulk data loading, use the proprietary ClickHouseColumnWriter API. This is accessed via ClickHouseConnection.CreateColumnWriterAsync(). It allows you to write data in a columnar format, which is significantly more efficient for ClickHouse than standard row-based inserts.

    To use it, pass an array of columns (where each element is a collection representing a column) to WriteTableAsync.

    using var conn = new ClickHouseConnection("Host=127.0.0.1");
    conn.Open();
    using var cmd = conn.CreateCommand("CREATE TABLE IF NOT EXISTS table_with_two_fields(id Int32, name String) engine Memory");
    await cmd.ExecuteNonQueryAsync();
    
    // generate values
    List<int> ids = Enumerable.Range(1, 10_000).ToList();
    List<string> names = ids.Select(i => $"Name #{i}").ToList();
    
    // insert data
    await using (var writer = await conn.CreateColumnWriterAsync("insert into table_with_two_fields(id, name) values", default))
    {
    	await writer.WriteTableAsync(new object[] { ids, names }, ids.Count, default);
    }
  11. Insert data using parameters

    master

    You can perform inserts using parameterized commands to ensure data safety. Use cmd.Parameters.AddWithValue to bind values to placeholders in your SQL string.

    var sb = new ClickHouseConnectionStringBuilder();
    sb.Host = "127.0.0.1";
    using var conn = new ClickHouseConnection(sb);
    conn.Open();
    using var cmd = conn.CreateCommand("INSERT INTO table_you_just_created SELECT {id}, {dt}");
    cmd.Parameters.AddWithValue("id", Guid.NewGuid());
    cmd.Parameters.AddWithValue("dt", DateTime.Now, System.Data.DbType.DateTime);
    var _ = cmd.ExecuteNonQuery();