ClickHouse: что работает иначе¶
В ClickHouse нет частей SQL, на которые hare иначе опирался бы: ограничений уникальности и внешних
ключей, UPDATE на месте, RETURNING, ON CONFLICT, блокировок строк, числа строк, изменённых записью.
Транзакции и генерируемые ключи есть только на сервере с ClickHouse Keeper. Каждая строка ниже говорит, что
делает hare: где у ClickHouse есть другая запись того же SQL, hare пишет её; где в языке её нет, вызов
отклоняется до отправки SQL.
NullIf() требует тип результата, который ClickHouse может обернуть в Nullable. Для массивов,
кортежей, map, nested, Dynamic, Variant и других типов без такой обёртки вызов поднимает
UnSupportedError до SQL. Dynamic и Variant сами могут содержать null, но NULLIF в ClickHouse
всё равно требует неподдерживаемую обёртку Nullable.
| Чего нет в ClickHouse | Что делает hare |
|---|---|
| Транзакций по умолчанию | atomic() даёт UnSupportedError, если подключение не просит их через transactions=true, — см. Транзакции и блокировки. Без них собственные многокомандные записи hare идут одна за другой, и сбой оставляет записанными команды до него. |
| Точек сохранения | Вложенный atomic() присоединяется к внешней транзакции; ошибка, покинувшая его, откатывает всю транзакцию. |
| Генерируемых ключей | С ClickHouse Keeper ключи берутся из серии до записи строк (generateSerialID) — см. Модели. Без него модель, ключ которой генерирует база, отклоняется при привязке. |
| Ограничений уникальности и внешних ключей | Уникальность или связь с db_constraint=True отклоняется — у модели при привязке (ConfigurationError), у миграции, объявляющей уникальность, до её первой команды (UnSupportedError); см. Модели. |
CHECK, пропускающего строку, условие которой даёт NULL |
ClickHouse такую строку отвергает (IntegrityError): CheckConstraint(check=Q(price__gte=0)) отвергает price=None — проверка, пропускающая NULL, называет его: Q(price__isnull=True) \| Q(price__gte=0). Проверка, которую добавляет миграция, проверяет только строки, записанные позже, — см. Операции. |
UPDATE строк на месте |
save(), QuerySet.update() и bulk_update() выполняют мутацию ALTER TABLE ... UPDATE, завершённую до возврата из вызова — см. UPDATE строк на месте |
| Числа строк, изменённых UPDATE или DELETE | update(), delete() и bulk_update() возвращают None (Features.reports_written_row_count) — см. Число записанных строк |
RETURNING |
returning() у update() или delete() и bulk_create(returning=True) дают UnSupportedError; значения, которые сервер дал записанной строке, остаются непрочитанными в экземпляре — см. RETURNING |
ON CONFLICT |
bulk_create() с ignore_conflicts, update_fields, on_conflict или on_conflict_constraint даёт UnSupportedError (Features.supports_insert_conflicts); transactional outbox, который так пишет свои события, отклоняется при привязке его модели — см. Outbox. |
| Батчей у UPDATE | BackfillColumn заполняет все строки одной мутацией, batch_size не используется: мутация не блокирует строк, которые пощадил бы батч, и каждая переписывает изменяемые части. |
| Блокировок строк | select_for_update() и atomic(lock_timeout=...) дают UnSupportedError — см. Транзакции и блокировки. |
NULL от агрегата по пустому набору |
Агрегат, результат которого на пустом наборе был бы значением, которого нет ни в одной строке, пишется в форме -OrNull (maxOrNull(...)) и даёт None; Sum даёт собственный 0 ClickHouse (None для поля с null=True) — список см. в NULL от агрегата по пустому набору |
LIKE ... ESCAPE |
Шаблон экранируется обратной косой чертой — собственным символом экранирования ClickHouse. Пустой текст содержится в любом тексте, как в PostgreSQL (contains="", contains=F("name") при пустом имени): ClickHouse не сопоставляет пустую строку шаблону '%%', поэтому hare пишет такой шаблон как '%'. |
Простого UPDATE и отказа в NULL в готовом SQL |
Ваш SQL (RunSQL, raw(), execute()) выполняется так, как его принимает ClickHouse: UPDATE нужна настройка enable_block_number_column (ALTER TABLE ... UPDATE — нет), а NULL в колонке не Nullable пишется пустым значением типа (0, ''). Записи hare такой None отвергают. |
| Обратной косой черты как обычного символа имени в кавычках | Имя с обратной косой чертой — таблицы, колонки, псевдонима — пишется с удвоенной чертой: одиночную ClickHouse читает как экранирование. |
Рекурсивного запроса, добавляющего строки через UNION DISTINCT |
with_recursive() нужен max_depth, который заканчивает цикл в строках, — см. Рекурсивные запросы и CTE. Без него обход даёт UnSupportedError. |
NULL первым при сортировке по убыванию |
ClickHouse ставит NULL последним в обоих направлениях — см. NULL первым при сортировке по убыванию |
Ключ с частью NULL в __in |
Q(relation__in=[...]) связи с составным ключом сравнивает кортеж колонок: для строки, ключ которой содержит NULL, ClickHouse даёт ложь там, где SQLite и PostgreSQL дают NULL, — filter() берёт те же строки, а условие в аннотации (annotate(x=Q(...))) держит False, а не None |
| Курсора транзакции | stream() читает строки и вне транзакции: сервер отдаёт их по мере вычисления. Ранний выход из async for закрывает поток и возвращает соединение. |
| Смещения зоны от UTC с секундами | Читается без секунд — см. Смещения зоны от UTC с секундами. |
Смещения зоны от UTC с секундами¶
ClickHouse хранит смещение зоны в целых минутах: местное среднее время до стандартного времени зоны
(Москва до 1919 года, +02:30:17) читается без секунд — Extract("second"), Trunc() до суток и
длиннее и __time такого момента в этой зоне (или при use_timezone=False в ней) отличаются от
PostgreSQL на эти секунды; Trunc() до часа, минуты или секунды берёт смещение целиком.
UPDATE строк на месте¶
save(), QuerySet.update() и bulk_update() выполняют мутацию ALTER TABLE ... UPDATE, завершённую
до возврата из вызова. Таблица с ClickhouseTableOptions(lightweight_updates=True) получает вместо неё
лёгкий UPDATE — см. Модели. delete() выполняет
лёгкий DELETE, а если его условие читает union()/intersection()/difference(), — мутацию
ALTER TABLE ... DELETE: лёгкий DELETE сервера на таком условии падает. Упавшая мутация
останавливается (KILL MUTATION) до того, как вызов выдаст ошибку: иначе сервер повторял бы её, из-за
неё падала бы каждая следующая мутация таблицы, и она ещё могла бы примениться после ошибки вызова.
Мутация не выполняет связанных подзапросов: update(), который записывает в поле Subquery с
OuterReference, фильтруется или удаляет по такому подзапросу, даёт UnSupportedError. Начиная с
ClickHouse 26.7 сервер отклоняет мутацию, которая записывает в поле без NULL выражение, способное быть
NULL, — Subquery поля с null=True (CANNOT_INSERT_NULL_IN_ORDINARY_COLUMN), — даже когда его
значение не NULL; такое выражение оберните в Coalesce() со значением. Мутация
bulk_update() пишет не больше 1000 строк (Features.max_bulk_update_rows): значения каждой строки она
находит перебором списка строк, поэтому более длинный список дороже на строку и выходит за предел AST
сервера. Ключи её строк читает подзапрос самого списка, одинаковый на каждой реплике: она выполняется с
allow_nondeterministic_mutations = 1, без чего таблица ReplicatedMergeTree не принимает мутацию с
подзапросом, — update(), отфильтрованный подзапросом другой таблицы, сервер там отвергает, ведь реплики
могут прочитать разные строки.
Одновременные мутации одной таблицы — собственное поведение сервера: каждая читает строки такими, какими
они были при её начале, поэтому два одновременных update(n=F("n") + 1) одной строки могут оба
записать n + 1 — одно приращение теряется, тогда как блокировка строки в PostgreSQL выполнила бы их
по очереди. Выполняйте такие обновления одной строки последовательно.
Число записанных строк¶
ClickHouse не сообщает, сколько строк изменила мутация или лёгкий UPDATE/DELETE, поэтому update(),
delete() и bulk_update() возвращают None; чтобы узнать, что изменилось, прочитайте строки. Удаление,
которое hare выполняет сам, — каскад в Python, модель с переопределённым delete() — считает прочитанные
для него строки. Без числа строк:
save()илиdelete()строки, которую удалил другой клиент, не обнаруживается —DoesNotExistне возникает;- модель с
Meta.optimistic_lock_fieldотклоняется при привязке: устаревшая версия осталась бы незамеченной; bulk_update()считает записанным каждый объект и применяет ко всем его версию, снимок изменённых полей и значенияauto_now.
add()/set() обратной связи вместо этого сначала читает строки — строка, которую скрывает область по
умолчанию связанной модели (чужого арендатора, мягко удалённая), отклоняется IntegrityError до записи,
как и на других базах.
RETURNING¶
update().returning(), delete().returning() и bulk_create(returning=True) дают UnSupportedError до
отправки SQL. Значение, которое сервер даёт строке, записанной save() или create(), — db_default,
колонка MATERIALIZED — остаётся в экземпляре непрочитанным: поле держит DatabaseDefault,
вычисляемое поле не загружено, а refresh_from_db() читает их. Ключ, который генерирует сервер, берётся до
вставки (см. Модели); модель, ключ которой оставлен db_default, отклоняется при
записи строки.
NULL от агрегата по пустому набору¶
Max, Min, Avg, StdDev, Variance и собственные агрегаты ClickHouse ArgMax, ArgMin, AnyValue,
AnyLast, Quantile и Median (и с exact=True) пишутся в форме -OrNull
(maxOrNull(...)), поэтому на пустом наборе дают None, а не значение типа колонки по умолчанию;
Count даёт 0, а Sum — собственный 0 ClickHouse (0.00 для десятичного; None для поля с
null=True, и на наборе только из NULL тоже). В запросе с GROUP BY
в группе есть хотя бы одна строка, поэтому агрегат без _filter остаётся в простой и более быстрой форме
(max(...)). Выборочное отклонение по одному значению — None, как в SQL, а не NaN ClickHouse.
Результаты самого ClickHouse¶
hare не заставляет ClickHouse давать результат PostgreSQL. Она проверяет то, что гарантирует сама, —
значение, которое записывает, член перечисления, — пишет формы, которые хранит сама (перечисление по
метке, адрес как IPv6, путь JSON), и выполняет свои функции собственными средствами ClickHouse; всё
остальное — результат самого ClickHouse, и он отличается от результата PostgreSQL:
- Деление и остаток от деления на ноль дают
infилиnanдляfloatи собственную ошибку ClickHouse для целых; математическая функция вне области определения (Sqrt(-1),Ln(0)) даётnanилиinf. hare не проверяет ничего из этого.MODи отрицательный ноль — тоже собственные. - Арифметика десятичных — собственная ClickHouse. У частного столько знаков после запятой, сколько
у делимого, лишние отсекаются, а не округляются (
F("price") / 8от12.34—1.54). Сумма или произведение сверх цифр своего типа «заворачивается» (Decimal128сверх 38 цифр), десятичное рядом с целым сохраняет собственный тип (Decimal(9, 4)плюс большоеInt64«заворачивает» цифры целого), а произведение сверх цифр типа результата даёт ошибку ClickHouseDecimal math overflow.Avg(),StdDev()иVariance()десятичных — числа с плавающей точкой (Features.computes_decimal_statistics_exactly);Sum()десятичных —Decimal(38, знаки), квантиль остаётся десятичным.Round()— этоround()ClickHouse. Значение, которое сервер вычислил шире 38 цифр (Sum()отDecimal128сверх них), clickhouse-driver читает со всеми цифрами, а clickhouse-connect при чтении округляет до 38 цифр — это поведение самой библиотеки. - Количества, длины, номера строк и ранги (
Count(),Length(),StrIndex()и оконные функцииRowNumber(),Rank(),DenseRank(),NTile()) — знаковыеInt64: собственныйUInt64ClickHouse не находит общего типа со знаковым целым вCoalesce(),Case()иGreatest(). - Целочисленная арифметика — собственная ClickHouse: результат за пределами диапазона своего типа
«заворачивается» (
9223372036854775807 + 1— это-9223372036854775808), как и значение, которое записываетupdate(n=F("n") + 1)(2147483647 + 1хранится как-2147483648вInt32), иSum()иAvg()целых, которые суммируются в типе колонки (Avg()двух значенийUInt642**64 - 1— это2**63);Abs()минимума типа — сам минимум./двух целых даёт точное частное, в том числе дробь (Features.divides_integers_exactly), — поле вывода выраженияFloatField. Десятичное, которое выражение вычисляет с большим числом знаков, чем у поля, ClickHouse отсекает, а не округляет:update(price=F("price") + Decimal("0.005"))оставляет12.34, где PostgreSQL и SQLite пишут12.35. Cast()— собственное преобразование ClickHouse. К целому оно отбрасывает дробную часть, тогда как PostgreSQL округляет, и «заворачивает» число за пределами диапазона цели (Cast(Value(3e9), IntField())— это-2147483648); к десятичному отсекает знаки сверх знаков цели,floatпреобразуется как то двоичное число, которым он является (2.675становится2.67). Текст читают парсеры ClickHouse: текст, не являющийся числом, —NULL, как и число, обрамлённое пробелами (' 42 '); дату или момент читаетparseDateTime64BestEffort()— дата без части дополняется ("2024-01"— первое января), время за пределами суток переносится ("25:00"— час ночи следующего дня), год вне 1900–2299 становится ближайшим днём, который хранит ClickHouse, — начиная с ClickHouse 26.7, гдеDateTime64хранит годы с 1 по 9999, такой год читается как написан, а дата такого года после 2299 — это деньDate32, который clickhouse-driver не читает (его собственныйKeyError), — а дня, которого нет в календаре ("2023-02-29"), даёт исключение. Приведение к логическому —toBool()ClickHouse, который принимает иyes,no,on,offи даёт исключение на любом другом тексте. Приведение к тексту —toString():1e308,-inf, десятичное без хвостовых нулей, момент в виде2024-01-02 03:04:05.678900.max_lengthуCharFieldничего не обрезает.- Сдвиг момента на длительность — собственный
addMicroseconds()ClickHouse: момент за пределами лет, хранимых колонкой, становится ближайшим из тех, что она хранит, —DateTimeс 1900-01-01 по 2299-12-31 23:00 (DateTime64начиная с ClickHouse 26.7 — годы с 1 по 9999),Date32— до 2299-12-31. union(),intersection()иdifference()упорядоченных ветвей не оставляют дублей: подключение работает с выключенной настройкой ClickHouseoptimize_distinct_in_order, которая оставляла дубли таких ветвей, — в том числе внутри подзапроса, собственныеSETTINGSкоторого операция над множествами не читает.Concat()склеивает текст, который ClickHouse даёт значению колонки (toString()); наивное дата-время (use_timezone=False) пишется по местному поясу машины. Значение Python, переданное литералом, — это его текст в Python: логическоеtrueилиfalse,floatчерезrepr()(1000000000000000.0), дата или дата-время — текст ISO. Текстовый оператор фильтра (contains,startswith,iexact, ...) для колонки логического,floatили десятичного типа сопоставляет текст, который ClickHouse даёт значению.- Смена регистра.
Upper(),Lower()и операторы фильтраi...идут черезupperUTF8()иlowerUTF8()— собственные функции сервера; операторыi...сравнивают текст в верхнем регистре, поэтомуiexact="ß"находит иSS, иss(upperUTF8('ß')— этоSS). Substr,ChrиOrd— этоsubstringUTF8()(начало 0 отклоняется, отрицательное начало считается с конца, отрицательная длина оставляет символы с конца),char()(кодовая точка, которой не соответствует ни один символ, отклоняется, 0 даёт\x00) и кодовая точка первого символа дляOrd. У текстовых функций нет 32-битного предела длины, позиции и количества, они принимают выражение любого типа; литерал не того типа отклоняется.Lag/Leadс JSON-default— массивом значений разных типов ([1, 2.5, None, "x"]) — через HTTP-драйвер падают сNO_COMMON_TYPE, хотяCASTтого же JSON проходит: так работают оконные функции самого сервера (проверено на ClickHouse 25.8.33.6).FIRST_VALUEиLAST_VALUE— собственные функции ClickHouse, как и порядок UUID (сначала младшие 64 бита: ключuuid7не сортируется по времени, сортируйте по колонке с датой) и адресовIPv6. Перечисление упорядочивается по метке: так его хранит hare. Время суток упорядочивается и сравнивается как хранимый текст:10:00:00+03:00идёт после08:00:00+00:00, хотя это более ранний момент.NaNвFloatFieldхранится. Нулевой байт в тексте даётValidationError— значение пишется в текст SQL, который его не вмещает; внутри объекта JSON он хранится так, как его хранит ClickHouse.- Ошибки. Перевод колонки в
NOT NULL, пока в ней естьNULL, даётcolumn X of table Y holds NULL.
NULL первым при сортировке по убыванию¶
ClickHouse ставит NULL последним в обоих направлениях; hare пишет сортировку по убыванию как
DESC NULLS FIRST, так что NULL встаёт последним по возрастанию и первым по убыванию, и страницы
после курсора, reverse() и last() не теряют строк. ValueRange окна со смещением сортирует по
значению как по Float64, а NULL — как по бесконечности с той стороны, куда встаёт NULL:
собственный диапазон ClickHouse вокруг NULL захватывает другие строки.
ArrayAgg(order_by=...) ставит NULL в значении сортировки туда, куда его ставит сортировка.
Длинные списки __in¶
Сервер медленно разбирает список литералов: 100 000 значений занимают почти секунду. Список __in или
__not_in из 500 и более значений одного простого типа — UUID, целых, текстов, дат или строк из них для
ключа из нескольких колонок — передаётся одним параметром: чтение отправляет значения рядом с командой
внешней таблицей (x IN hare_set_1), которую сервер читает как данные за несколько миллисекунд, а любая
другая команда (мутация, DELETE), как и чтение в транзакции, которая не читает внешних таблиц, пишет их
списком литералов. SQL запроса один и тот же при любой длине списка, поэтому у него один план.
Индексы — это индексы пропуска данных: Index(fields=...) создаёт индекс minmax с гранулярностью 1, а
остальные типы есть в hare.dialects.clickhouse.indexes — см.
Модели. hare inspectdb и hare drift читают таблицы, колонки,
первичные ключи, комментарии, индексы пропуска, опции таблиц, представления и словари из таблиц system.
Связанные подзапросы¶
ClickHouse выполняет связанные подзапросы, но часть из них считает неверно или отклоняет:
- Связанный подзапрос внутри
IN/NOT INClickHouse 25.8, 25.10, 25.12, 26.3, 26.7, 26.8 и 26.9 отклоняет сUnSupportedErrorот сервера, даже когда связанный скалярный подзапрос выполняется. EXISTS, связанный равенством колонок — и строки колонок составного ключа, — пишется как(columns) IN (SELECT ...)— собственный связанныйEXISTSсервер не выполняет вONсоединения и надUNION DISTINCT(25.8).EXISTSнадunion()пишется какEXISTSлюбой из частей, каждая так же. Связанный в своёмWHEREне только равенствами (version__gt=OuterReference("version"), какLatestVersions) выполняется как собственныйEXISTSсервера; вUPDATEилиDELETEили связанный вне своегоWHEREон даётUnSupportedError. Форма, которую сервер пока не выполняет, даётUnSupportedErrorс его сообщением.- Группировка по связанному подзапросу (
values("author_name").annotate(...)надSubqueryсOuterReference) даётUnSupportedError— сервер по таким не группирует. - Подзапрос, который сам сортирует или срезает свои строки (
Subquery(... .order_by("-created")[:1])сOuterReference), даётUnSupportedError— сервер его отклоняет. - Подзапрос, выбирающий
Count,Sumили другой агрегат своих строк и связанный по колонке, общей у строк внешнего запроса (Book.objects.annotate(n=Subquery(Award.objects.filter(author=OuterReference( "author"))...))), даётUnSupportedError(Features.supports_correlated_aggregates): сервер читает каждую его строку столько раз, сколько внешних строк держат то же значение, и столько же раз её считает. Связанный по первичному ключу или уникальному полю внешней модели (OuterReference("pk")) он работает, как иMax,MinиAvgпо любой колонке. Считайте через связь —annotate(Count("books")). - Подзапрос в
ORDER BYили выбранный рядом сWHEREвычисляется в производной таблице: строки выбираются безWHERE, условия и ключи сортировки становятся её скрытыми колонками, а внешний запрос фильтрует, сортирует и срезает их. SQL длиннее, результат — тот, что просит запрос. UPDATEилиDELETE, в условии которого есть связанный подзапрос, даётUnSupportedError— мутация их не выполняет.OuterReferenceчерез связь (OuterReference("author__name")) читает колонку таблицы, присоединённой к запросу вокруг подзапроса, а по таким сервер не связывает: этоUnSupportedError. Пройдите связь внутри подзапроса и свяжите его колонкой самой внешней таблицы (Author.objects.filter(books__id=OuterReference("id"))) или прочитайте поле связи без подзапроса (F("author__name")).- Таблица, которую подзапрос присоединяет, читается через производную таблицу, как и его собственная: в ClickHouse таблица с псевдонимом отзывается и на своё имя, и таблица подзапроса забрала бы имя внешнего запроса.
Ошибки версии сервера¶
hare не обходит ошибку версии ClickHouse; остаётся собственная ошибка или собственный результат сервера:
- Колонка, ставшая
Nullable(AlterFieldнаnull=True): ClickHouse до 26.3 читает карту NULL, которой нет у частей, сохранённых до изменения, как NULL во всех значениях —filter(title__isnull=True)находит все такие строки. На этих версиях перепишите колонку после миграции сами:ALTER TABLE "article" UPDATE "title" = "title" WHERE 1(вRunSQLпослеAlterField). До этого так же читают и проверки следующих миграций колонки: откат изменения к NOT NULL отклоняется сIntegrityError(holds NULL), а значение такой части, которое не переводится в новый тип при смене типа, до изменения не находится. - Сортировка по пути JSON (
order_by("data__n")): ClickHouse 25.8 сallow_simdjson=0, который hare ставит, чтобы читать целые больше 64 бит, в зависимости от данных может поставить некоторые строки колонкиJSONне на своё место. Там сортируйте по приведению пути (Cast("data__n", IntField())). update(), ставящий константу под отрицанием, которое сворачивается в константу (exclude(owner__isnull=True).update(balance=0)приownerбез NULL): ClickHouse до 26.3 отказывает с Cannot convert column ... because it is non constant in source stream.- ClickHouse 25.12 отказывает запросу, который соединяет связь многие-ко-многим ради агрегата рядом с
объектом
select_related()(Player.objects.annotate(Count("skills")).select_related("team")), с Cannot fold actions for projection (LOGICAL_ERROR) — ошибка его перестановки соединений; 25.10 и 26.x выполняют его.
Порядок текста¶
ClickHouse упорядочивает и сравнивает текст по его байтам UTF-8, как SQLite по умолчанию; PostgreSQL — по
правилу сравнения (collation) своей базы, порядок локали в котором ставит a рядом с A, а буквы с
диакритикой — рядом с их основой. order_by(), __gt/__lt и Min/Max у CharField, TextField и
CharEnumField (сравнивается по метке) следуют порядку базы; Collate(...) просит у базы свой порядок —
в ClickHouse только в order_by() (order_by(Collate("name", "en")) пишется как
ORDER BY name ASC COLLATE 'en'): больше нигде сервер правило сравнения не принимает.
JSON¶
JSONField — колонка JSON, пути которой хранятся типизированными подколонками (кроме payload и headers
outbox — см. Outbox); через clickhouse-driver, который не читает
колонку JSON, это колонка String с текстом JSON, которую читают функции JSON*. Какая из них у
подключения, следует из его драйвера ещё до открытия — hare drift, hare sqlmigrate и миграции сравнивают,
печатают и создают колонку этого подключения. Таблицу, созданную через один драйвер, через другой тоже читают,
пишут и фильтруют, кроме ключа с точкой: его пишут и по нему фильтруют через драйвер, создавший таблицу —
через другой clickhouse-connect пишет его в колонку String с экранированной точкой (%2E), а
clickhouse-driver не находит такой ключ колонки JSON; читается он верно через оба; hare drift через другой
сообщает AlterField колонки — они по-разному хранят null, см. ниже. Тип JSON хранит меньше, чем текст
JSON, и значение, которое он прочитал бы иначе, даёт ValidationError до отправки — колонка String через
clickhouse-driver хранит всё это:
- Хранится только объект: список, строка или число на верхнем уровне отклоняются.
- Ключ со значением
Noneили пустым объектом на любой глубине отклоняется ({"a": None},{"items": [{"b": {}}]}) — такого пути тип не хранит.Noneили пустой объект внутри массива ([None, 1],[{}]) хранится. Поэтомуfield__path=None,Noneвfield__path__in=[...]иfield__path__isnull=Trueнаходят путь без значения — у строки, всё значение которой NULL, пути нет, —field__path__not=NoneиNoneвfield__path__not_in=[...]его исключают, а сравнение пути (field__path__lt=10) его не находит. - Целое вне диапазона
Int64иUInt64(от-2**63до2**64 - 1) отклоняется — тип хранит его какfloat. - clickhouse-connect задаёт тип пути строк одного
bulk_create()по первому встреченному значению: целое после дробного по тому же пути пачки хранится и читается какfloat, и фильтр по его точному значению ничего не находит. Держите числа пути в пачке одного типа или пишите такие строки по одной; clickhouse-driver хранит текст JSON как есть. - Порядок ключей не сохраняется.
- Точка в ключе остаётся частью ключа: каждое соединение задаёт
json_type_escape_dots_in_keys, а hare пишет точку так, как её экранирует сервер, —%2E, в том числе в мутацииupdate(), которая читает JSON с собственными настройками сервера. Ключ, в котором уже есть текст%2E, даётValidationError: сервер прочитал бы его обратно как точку. - Массив целых чисел и
floatхранит их все какfloat:[1, 2.5]читается как[1.0, 2.5]; массив, целые которого (в том числе во вложенных массивах) больше2**53— егоfloatточно не хранит, — отклоняется, как и массив, где логическое значение стоит рядом с числом или строкой: мутация прочитала бы[true, 1]как[1, 1], а["", true]— как["", "true"]. Логическое значение остаётся логическим, а строка — строкой, даже похожая на дату или момент ("2024-01-02"): каждое подключение выключаетinput_format_try_infer_datesиinput_format_try_infer_datetimes. Ноupdate(),save()иbulk_update()пишут мутацией, которую ClickHouse выполняет с собственными настройками сервера, что бы ни ставило подключение: строка, похожая на дату, хранится путёмDateилиDateTimeи читается какdateилиdatetime, если профиль сервера по умолчанию не ставит эти две настройки в0.
Пути ключей, __has_key, __has_keys и __has_any_keys работают на обоих типах; __filter,
__contains и __contained_by не работают — в JSON ClickHouse нет вхождения, и они дают
UnSupportedError; фильтруйте по путям ключей. __has_key, __has_keys и __has_any_keys находят только
ключ объекта (JSONHas()). Миграция переводит колонку String в JSON через MODIFY COLUMN.
JSONField, сравниваемый целиком (=, __not, __in, __not_in) в колонке JSON, приводится к
JSON и сравнивается самим ClickHouse, поэтому порядок ключей не важен; __in сравнивает так каждое
значение, через OR. Колонка JSON хранит только объект: сравниваемое значение, которое не объект
(список, строка, число), даёт ValidationError. В колонке String с текстом JSON — через clickhouse-driver —
сравнивается текст JSON в том виде, в каком он записан.
Сравнение порядка пути (__gt, __gte, __lt, __lte, __range, order_by()) использует значение
Dynamic, которое ClickHouse читает из текста JSON (JSONExtract(..., 'Dynamic'), настройка сессии
allow_suspicious_types_in_order_by=1). ClickHouse упорядочивает Dynamic сначала по имени его типа,
потом по значению: Float64 2.5 идёт перед Int64 9, логическое — перед числами, а строки — после.
Сравнение пути, в котором лежат и строки, и числа, даёт NO_COMMON_TYPE ClickHouse. Порядок jsonb
(объекты, массивы, логические, числа, строки, null) есть только в PostgreSQL, а сравнение с объектом или
массивом — собственное сравнение ClickHouse.
Cast(..., TextField()) и Concat от JSONField дают текст, который пишет ClickHouse: порядок ключей
колонки (собственный у типа JSON, записанный — в колонке String) и её запись числа (1e+20, 12.0).
Текстовый оператор фильтра по пути сопоставляет текст, который ClickHouse читает из значения.
JSONObject() и JSONArray() пишет toJSONString(): десятичное без хвостовых нулей, DateTime64 в виде
2024-02-29 13:45:06.789000, NaN как null, байты — шестнадцатеричной записью, которую хранит колонка.
BinaryField — колонка String с шестнадцатеричной записью байтов строчными буквами: clickhouse-connect
читает String как текст и пишет байты, не являющиеся UTF-8, их шестнадцатеричной записью, поэтому сырые
байты нельзя было бы прочитать обратно. Шестнадцатеричная запись читается обратно точно через оба
драйвера и упорядочивается так же, как байты; unhex() даёт байты в SQL.
Массовая загрузка¶
bulk_create() отправляет строки одной двоичной вставкой — в формате ClickHouse Native, колонка за
колонкой — вместо текста INSERT ... VALUES: литералы для значений не пишутся, а сервер не разбирает
SQL. Так строки и должны попадать в ClickHouse, и поэтому много строк стоит писать через bulk_create(),
а не через create() в цикле.
batch_sizeделит объекты на столько вставок; без него они идут одной.- Значения — те же, что пишет
create(): момент — это мгновение, которое он обозначает (момент без часового пояса приuse_timezone=False— мгновение его местного времени, и читается он обратно как это местное время), значение JSON — его текст или объект. - Наблюдатели, спаны и счётчик запросов видят вставку как
INSERT INTO <table> (<columns>) FORMAT Native— строки не входят ни в какой текст SQL. use_copy=Trueпросит ту же загрузку и здесь ничего не меняет.returning=Trueи параметры конфликтов даютUnSupportedError.- С
async_insert=trueсервер собирает строки многих вставок, прежде чем записать их; сwait_for_async_insert=falseвызов возвращается до записи и не сообщает об ошибке.