ClickHouse: типы¶
У каждого поля hare есть тип колонки ClickHouse; hare.dialects.clickhouse.fields добавляет типы,
которые есть только в ClickHouse, а контейнеры из hare.fields держат любые поля — и друг друга — на
любой глубине. hare inspectdb записывает каждый из них обратно своим полем, а hare drift сравнивает
их целыми деревьями.
Поля и их колонки¶
| Поле | Колонка |
|---|---|
SmallIntField, IntField, BigIntField |
Int16, Int32, Int64 |
Int8Field, UInt8Field, UInt16Field, UInt32Field, UInt64Field, UInt128Field, UInt256Field, Int128Field, Int256Field |
Int8, UInt8 ... Int256 — hare.dialects.clickhouse.fields, каждое — ClickhouseIntegerField, принимающее только значения своего типа; Int8Field — колонка знака CollapsingMergeTree |
FloatField, Float32Field |
Float64, Float32 |
DecimalField(max_digits, decimal_places) |
Decimal(P, S) |
BooleanField |
Bool |
CharField, TextField |
String |
FixedStringField(length) |
FixedString(length) |
DatetimeField |
DateTime64(6, 'UTC') — микросекунды, хранится в UTC |
DateField |
Date32 |
ClickhouseDatetimeField(precision=None, timezone="UTC") |
DateTime('UTC') — целые секунды; DateTime64(precision, 'UTC') при заданном precision |
ClickhouseDateField |
Date |
TimeField |
String — время суток в ISO, сортируется и сравнивается как этот текст (10:00+03:00 после 08:00+00:00); чтобы упорядочивать моменты, храните их в DatetimeField |
TimeDeltaField |
Int64 — микросекунды |
UUIDField |
UUID — упорядочивается и сравнивается в собственном порядке ClickHouse, который берёт сначала младшие 64 бита: ключ uuid7 здесь не сортируется по времени, поэтому сортируйте по колонке с датой |
BinaryField |
String с шестнадцатеричной записью байтов (строчными буквами) |
JSONField |
JSON, String через clickhouse-driver — см. JSON |
CharEnumField, IntEnumField |
Enum8/Enum16 — см. Перечисления |
IPAddressField, IPv4AddressField |
IPv6 (адрес IPv4 отображается в него и читается обратно как IPv4; сортируется и сравнивается в собственном порядке ClickHouse для IPv6), IPv4 |
ArrayField, MapField, TupleField, NestedField |
Array, Map, Tuple, Array(Tuple(...)) — см. Контейнеры |
LowCardinalityField(field) |
LowCardinality(T) |
VariantField([...]), DynamicField() |
Variant(...), Dynamic |
GeometryField |
Point, LineString, Polygon, MultiLineString, MultiPolygon — см. Гео |
Колонка поля с null=True — Nullable(...), но только у листа: массив, Map и кортеж никогда не бывают
Nullable (см. Контейнеры). Целая колонка при переполнении «заворачивается», поэтому hare
сам проверяет диапазоны целых и десятичных значений, которые записывает; беззнаковое 64-битное значение
выше 2⁶³ и 128- и 256-битные целые — это int Python. Целочисленная арифметика — собственная
ClickHouse: результат за пределом типа «заворачивается» и в запросе, который его читает
(annotate(next=F("total") + 1)), и в значении, которое записывает update()
(update(total=F("total") + 1) хранит 2147483647 + 1 как -2147483648 в Int32), — см.
Целочисленная арифметика. Остаток от деления целых (F("count") % -2)
вычисляется собственным modulo() ClickHouse, как и деление или остаток от деления на ноль (inf,
nan или собственная ошибка сервера для целых); hare ничего из этого не проверяет.
ClickHouse читает числовой литерал как самый узкий тип, который его вмещает, а у беззнакового и
отрицательного общего типа нет. Поэтому hare пишет целое за пределами Int64 через широкий тип
(toInt128('18446744073709551615')), значения строк bulk_update() и списка VALUES — как тип их
колонки, а ветку-литерал Case (или Coalesce) с целым результатом — как этот тип (accurateCast;
литерал, который тип не вмещает, остаётся как есть и читается типом, который его вмещает). Литерал
UUID читается как UUID (toUUID), а
ветка-словарь Case с результатом-словарём — как этот словарь (Map): Case, смешивающий литералы и
колонку того же типа, работает как в PostgreSQL.
Float32Fieldчитает значение как самую короткую десятичную запись того же float32 —0.1, а не0.10000000149011612, которое отдают драйверы; значение вне диапазона или настолько близкое к нулю, что сохранилось бы как 0 (1e-50), даётValidationError— float32 не держит ни того, ни другого. Сумма, среднее и арифметика его значений — этоFloat64, в котором их считает ClickHouse, и читаются какFloatField; статистика (StdDev,Variance) остаётся float32.FixedStringField(length)хранит текст не длиннееlengthбайт в UTF-8, дополненный нулевыми байтами, и читает его без них; текст, в котором есть свой нулевой байт, даётValidationError— сервер прочёл бы этот байт как конец текста.ClickhouseDatetimeFieldхранит момент в собственном типе ClickHouseDateTime(целые секунды, с 1970 по 2106) или, сprecision(от 0 до 9 знаков после секунды), вDateTime64(precision)(с 1900 по 2299), в часовом поясе колонкиtimezone— имя IANA,Noneдля пояса сервера; читает и пишет какDatetimeField, и у него те же операторы фильтра (__year,__hour, ...). Записываемый момент округляется до точности колонки, половина — от нуля, какDecimalFieldокругляется до своих знаков; момент вне диапазона колонкиDateTimeдаётValidationError.ClickhouseDateFieldхранит день вDate(с 1970-01-01 по 2149-06-06), день вне этого диапазона даётValidationError. Оба поля — вhare.dialects.clickhouse.fields;inspectdbсопоставляет эти колонки с ними. Приведение кClickhouseDatetimeField(Cast(value, ClickhouseDatetimeField(precision=3))) переводит значение в его собственный тип — с его точностью и часовым поясом.
Перечисления¶
CharEnumField и IntEnumField — это Enum8/Enum16 из меток и чисел. ClickHouse хранит числа, а
метки читает из типа, поэтому метка сохраняет своё число навсегда:
- метки
IntEnumField— имена членов, пронумерованные их значениями; - метки
CharEnumField— хранимые значения, пронумерованные по контрольной сумме самой метки: добавление, удаление или перестановка члена не меняет числа других меток. Сервер сравнивал бы и сортировал колонку по этим числам, поэтому там hare читает её как метку (toString(status)):order_by(),__gtиMin/Maxследуют тексту, как в PostgreSQL.
Миграция меняет члены через MODIFY COLUMN. Перед удалением члена она проверяет, что ни одна строка его
не хранит, — сервер отказывается удалять используемую метку.
Колонка Enum16 не принимает арифметику, среднее и математические функции, поэтому IntEnumField
читается там по своему числу (toInt16(level)): F("level") + 1, Avg("level") и Abs("level") работают с
числом. Max/Min сохраняют член перечисления. update(), задающий IntEnumField выражением
(update(level=F("tiny"))), отклоняет число, которого нет ни у одного члена: мутация сервера записала бы его
в колонку, а драйверы потом читали бы его как None или падали.
Контейнеры¶
from hare import fields
class Delivery(Model):
tags = fields.ArrayField(fields.CharField(max_length=20))
prices = fields.MapField(fields.CharField(max_length=3), fields.DecimalField(max_digits=10, decimal_places=2))
point = fields.TupleField({"lon": fields.FloatField(), "lat": fields.FloatField()})
items = fields.NestedField({"isbn": fields.CharField(max_length=20), "quantity": fields.IntField()})
history = fields.ArrayField(
fields.MapField(fields.CharField(max_length=10), fields.ArrayField(fields.IntField(null=True)))
)
| Поле | Значение | Пути | Операторы |
|---|---|---|---|
ArrayField(field) |
список | tags__0 — элемент (с 0, отрицательный — с конца), tags__0_2 — срез, tags__len |
__contains, __contained_by, __overlap, __len, __item=(index, value) |
MapField(key_field, value_field) |
словарь | prices__eur — значение по ключу (для отсутствующего ключа — значение типа по умолчанию; сегмент с именем оператора — prices__in — это оператор, который словарь отвергает, как и массив), prices__keys, prices__values, prices__len |
__has_key, __has_keys, __has_any_keys, __len |
TupleField([...]) / TupleField({...}) |
кортеж / словарь по именам | point__0, point__lat |
операторы элемента |
NestedField({...}) |
список словарей | items__0 — строка, items__isbn — колонка всех строк, items__len |
операторы массива |
Каждое значение внутри контейнера пишется и читается полем, которое его держит, — десятичное число,
момент, UUID, перечисление на любой глубине, — а путь любой длины проходит сквозь них:
history__0__open__1__gt=3 — второе число под ключом open первого словаря. Оператор в конце пути —
оператор поля на этом месте.
NULL. Массив,Mapи кортеж в ClickHouse никогда не бываютNullable, поэтому уnull=Trueна них — на верхнем уровне или внутри другого контейнера — нет колонки: модель даётUnSupportedErrorпри привязке.None, переданный модели (Shipment(tags=None),create(tags=None)), даётQueryError, как у любого поля безnull=True, а незаданный контейнер илиNone, присвоенный позже, даётValidationErrorпри любой записи — сервер сохранил бы вместо него пустой массив или кортеж нулей.null=Trueу скалярного поля внутри делаетNullableэтот элемент.Min/Maxмассива,Mapили кортежа по пустому набору строк дают его пустое значение (пустой массив илиMap, кортеж нулей), а неNone.Lag/Leadконтейнера принимаютdefaultего типа: без него — за границей раздела значение NULL, а у контейнера нет формы NULL — и дляNthValueони даютUnSupportedErrorдо SQL.DynamicиVariantхранят NULL сами и принимают все три.GeneratedFieldс контейнернымoutput_fieldведёт себя так же, в том числе при агрегировании пустого набора.- Через связь. У связанной строки, которой нет, — родителя без детей, незаданного внешнего ключа —
колонки-контейнеры и геометрия читаются пустым значением, а не
None: вvalues(),F()иorder_by(). Фильтр и агрегат через связь всё же отличают её от строки: фильтр выполняется для отсутствующей строки только как__isnull=True(или отрицание__isnull=False), аCount,Min,MaxиArrayAggеё не учитывают. - Порядок. Массив, кортеж и
Mapупорядочиваются и сравниваются так, как их упорядочивает ClickHouse, поэлементно; значениеCharEnumFieldвнутри — по метке, так его хранит hare: вorder_by(),Min/Maxи в фильтре по пути (statuses__0__gt). - Ключи
Map— целые, тексты,FixedString, UUID, даты, моменты и перечисления (иLowCardinalityтекста или UUID) безNULL; другое поле ключа даётConfigurationErrorпри объявлении поля. Путь называет ключ его значением, как его читает поле ключа:counts__online— значение под членом перечисления"online". - Ошибки называют путь до значения, о котором они:
tags[2],prices['eur'],point.1. - Миграция сравнивает дерево поля целиком и меняет любой уровень через
MODIFY COLUMN; сужение на любом уровне сначала проверяется по данным.
ArrayField работает и в PostgreSQL — вложенный ArrayField(ArrayField(...)) там многомерный массив;
MapField, TupleField и NestedField есть только в ClickHouse.
LowCardinalityField¶
LowCardinalityField(fields.CharField(max_length=2)) хранит значения словарём различных — для колонки,
где их немного (страна, статус). Значения, операторы и пути — те же, что у обёрнутого поля; null=True у
него — это LowCardinality(Nullable(T)). Оборачивает он текст, FixedStringField или UUIDField:
LowCardinality перечисления, десятичного числа или момента ClickHouse не хранит, а числа, даты или
адреса — только при настройке сервера, поэтому другое поле даёт ConfigurationError, когда строится тип
его колонки. Контейнер он не оборачивает.
VariantField и DynamicField¶
from hare.dialects.clickhouse.fields import DynamicField, VariantField
reading = VariantField([fields.IntField(), fields.CharField(max_length=20)], null=True)
value = DynamicField(null=True)
VariantField хранит значение одного из типов своих полей. Значение берёт первое поле, чей тип у него в
точности, иначе первое, экземпляром типа которого оно является, иначе первое, которое его преобразует;
прочитанное значение возвращает поле его типа ClickHouse — поэтому давайте поля, которые читаются разными
типами Python. Фильтр находит только значения поля, к которому относится значение фильтра, а
reading__0__gt=3 читает значения поля на этой позиции, NULL для остальных.
DynamicField хранит значение любого типа — с тем типом, который у его значения в Python (целое как Int64,
текст как String, список как Array), — и читает его таким, каким записал; момент — в настроенном часовом
поясе, как его читает DatetimeField (записанный без пояса — это время UTC). max_types ограничивает число
типов, которые хранятся в собственных колонках. Фильтр находит только значения типа значения фильтра
(value=5 не находит "5"); путь с именем типа читает значения этого типа — value__String__startswith="a",
value__Int64__gt=3.
ClickHouse не сортирует строки по колонке Variant или Dynamic: order_by() по ней даёт
UnSupportedError — сортируйте по её пути. Колонка сама хранит NULL: её поля не принимают null=True.
Гео¶
GeometryField из hare.gis — колонка Point, LineString, Polygon, MultiLineString или
MultiPolygon только с x и y и без SRID — прочитанное значение получает SRID поля, а геометрия другого
SRID при записи, в фильтре и в сравнении на равенство даёт ошибку (ClickHouse не пересчитывает
координаты). Точка — кортеж, а линия и область — массивы: ни одна из них не бывает Nullable, поэтому
поле геометрии с null=True даёт UnSupportedError при привязке модели, а записанный None —
ValidationError. Пустая геометрия — собственное значение ClickHouse, и функция от неё даёт собственный
ответ или ошибку ClickHouse. Кольца многоугольника хранятся в той ориентации, в
какой их читают функции ClickHouse, — внешнее кольцо по часовой стрелке, — поэтому многоугольник,
заданный наоборот, читается с развёрнутыми кольцами.
hare.gis |
ClickHouse |
|---|---|
Точка в области (__intersects, __within, __contains, __covers, __covered_by, __disjoint) |
pointInPolygon — точка на границе считается внутри только на части сторон (у квадрата левая и нижняя, но не правая и верхняя): точку у границы проверяйте по расстоянию |
| Отношения двух областей | polygonsIntersect*, polygonsWithin*, polygonsEquals* |
__intersects, __equals, __disjoint двух точек |
сравнение координат — точка встречает только ту же точку |
Отношение или Distance линии |
UnSupportedError — у ClickHouse нет такой проверки |
Area, Perimeter, ConvexHull, Intersection, Union, SymmetricDifference областей |
polygonArea*, polygonPerimeter*, polygonConvexHullCartesian, polygons* |
Distance точек |
L2Distance; geoDistance в метрах для географии |
__dwithin |
расстояние, сравнённое с заданным |
Distance областей |
polygonsDistance* |
AsText |
wkt |
География (GeometryField(geography=True)) измеряется на сфере среднего радиуса WGS 84 функциями
*Spherical — в метрах и квадратных метрах, на больших расстояниях немного иначе, чем на эллипсоиде
PostGIS; функция, которая есть у ClickHouse только на плоскости (ConvexHull, polygonsEquals), для неё
даёт UnSupportedError — как и всё, для чего у ClickHouse нет функции.