Исходя из того, что предыдущую статью не заминусовали и даже не сильно критиковали, попробую продолжить серию и поделиться с проблемами некоторых различий типов данных в MS SQL и PostgreSQL.
Деньги
Извиняюсь за дословный перевод. Начну я с типа money. Этот тип есть в MS SQL и PostgreSQL и вроде бы даже имеет одинаковую точность. Вот только в MS SQL эта точность фиксирована и всегда четыре знака после десятичной точки. В PostgreSQL точность значений типа money зависит от lc_monetary, по умолчанию наследуемого из системной локали. В подавляющем большинстве случаев которые встречал - это все же два знака после десятичной точки, а вовсе не четыре, как в MS SQL.
Если в БД MS SQL в полях типа money хранятся значения не только наиболее популярных валют, требующих только два знака после десятичной точки, то использование типа money в PostgreSQL становится проблематичным.
Даже если в БД MS SQL стабильно используются только два знака после запятой, некоторые вычисления в SQL запросах могут в промежуточных значениях использовать все четыре знака после запятой, что приведет к различию в результатах.
Исходя из вышеперечисленного, я не рекомендую использовать тип money в PostgreSQL, даже если он был использован в MS SQL. Надежней конвертировать его в тип decimal(19,4), пусть даже ценой весьма незначительной потери производительности.
Даты и время
Не буду заострять внимание на том, что в MS SQL нет прямого аналога типу timestamp with time zone. Тип datetime в MS SQL имеет прямое соответствие timestamp without time zone. Тонкости использования этих типов в PostgreSQL обсуждались на Хабре, например, тут.
А вот те неприятности, которые может доставить наличие в MS SQL типа datetime2 стоит рассмотреть. Дело в том, что datetime2 поддерживает дискретность до 100 наносекунд (7 знаков после десятичной точки в секундах), тогда как timestamp в PostgreSQL - лишь 1 микросекунду (6 знаков после десятичной точки в секундах). Потеря одного знака точности может привести к изменению порядка следования записей при сортировке.
Если вдруг точность до 100 наносекунд жизненно необходима, то в PostgreSQL ее потребуется поддерживать отдельным полем типа smallint. Такого в своей практике не встречал. Но сталкивался с необходимостью модификации значений типа datetime2 таким образом, чтобы они стали различаться в пределах шести знаков после десятичной точки для сохранения порядка сортировки. Потеря точности в микросекундах или даже в десятках микросекунд, обычно, не критична. А в возможность последовательной вставки более миллиона записей в секунду в одну таблицу MS SQL верится с трудом.
Если для миграции данных или в период опытной эксплуатации используется TDS_FDW, то тут даты и время приносят дополнительные проблемы.
Начнем с уже рассмотренного выше datetime2. В дальнейшем будем считать, что у нас уже создан FOREIGN SERVER
CREATE SERVER IF NOT EXISTS tds_test
FOREIGN DATA WRAPPER tds_fdw
OPTIONS (servername 'ms-sql-server.domain.name',
tds_version '7.4', msg_handler 'notice');
CREATE USER MAPPING IF NOT EXISTS FOR you_pgsql_username
SERVER tds_test OPTIONS (username 'you_mssql_username',
password 'you_mssql_password');
Создадим в MS SQL таблицу, не забывая о том, что у аккаунта you_mssql_username, который мы выше использовали для доступа к MS SQL серверу, должны быть права на просмотр плана выполнения и на чтение из создаваемой таблицы.
USE test_db
GRANT SHOWPLAN TO you_mssql_username
DROP TABLE IF EXISTS tmp_dates
CREATE TABLE tmp_dates (
ID int NOT NULL,
Val1 datetime2 NULL,
Val2 datetime NULL)
INSERT tmp_dates (ID, Val1, Val2)
VALUES (1, '2000-01-01', '2000-01-01'),
(2, '2000-01-01 01:01:01.1234567', '2000-01-01 01:01:01.123'),
(3, NULL, NULL)
GRANT SELECT ON tmp_dates TO you_mssql_username
Создаем соответствующую таблицу в PostgreSQL
DROP FOREIGN TABLE IF EXISTS tds_test_tmp_dates;
CREATE FOREIGN TABLE IF NOT EXISTS tds_test_tmp_dates (
ID integer NOT NULL,
Val1 timestamp NULL,
Val2 timestamp NULL
) SERVER tds_test OPTIONS ( table_name 'test_db.dbo.tmp_dates',
row_estimate_method 'showplan_all' );
При попытке обратиться к этой таблице получаем:
SELECT * FROM tds_test_tmp_dates;
SQL Error [22007]: ERROR: invalid input syntax for type timestamp:
"Jan 1 2000 12:00:00:0000000AM"
Проблема именно в двоеточии вместо десятичной точки для типа datetime2. Пересоздаем внешнюю таблицу и проверяем с убранным двоеточием:
DROP FOREIGN TABLE IF EXISTS tds_test_tmp_dates;
CREATE FOREIGN TABLE IF NOT EXISTS tds_test_tmp_dates (
ID integer NOT NULL,
Val1 varchar NULL,
Val2 timestamp NULL
) SERVER tds_test OPTIONS ( table_name 'test_db.dbo.tmp_dates',
row_estimate_method 'showplan_all' );
DROP TABLE IF EXISTS tmp_dates;
CREATE TEMP TABLE tmp_dates (
ID integer NOT NULL,
Val1 varchar NULL,
Val2 timestamp NULL );
INSERT INTO tmp_dates (ID, Val1, Val2)
SELECT ID,
regexp_replace(Val1,'(:..:..):','\1.')::timestamp,
Val2
FROM tds_test_tmp_dates;
SELECT * FROM tmp_dates; /*
1 2000-01-01 00:00:00 2000-01-01 00:00:00
2 2000-01-01 01:01:01.123457 2000-01-01 01:01:01.123
3 NULL NULL */
Очень похожая проблема возникает даже просто с типом date. Опять создаем таблицу на MS SQL
DROP TABLE IF EXISTS tmp_dates
CREATE TABLE tmp_dates (
ID int NOT NULL,
Val date NULL)
INSERT tmp_dates (ID, Val)
VALUES (1, '2000-01-01'),
(2, NULL)
GRANT SELECT ON tmp_dates TO you_mssql_username
И внешнюю таблицу на PostgreSQL
DROP FOREIGN TABLE IF EXISTS tds_test_tmp_dates;
CREATE FOREIGN TABLE IF NOT EXISTS tds_test_tmp_dates (
ID integer NOT NULL,
Val date NULL
) SERVER tds_test OPTIONS ( table_name 'test_db.dbo.tmp_dates',
row_estimate_method 'showplan_all' );
SELECT * FROM tds_test_tmp_dates;
SQL Error [22007]: ERROR: invalid input syntax for type date:
"Jan 1 2000 12:00:00:AM"
И снова тоже самое двоеточие. Решение совершенно аналогичное.
DROP FOREIGN TABLE IF EXISTS tds_test_tmp_dates;
CREATE FOREIGN TABLE IF NOT EXISTS tds_test_tmp_dates (
ID integer NOT NULL,
Val varchar NULL
) SERVER tds_test OPTIONS ( table_name 'test_db.dbo.tmp_dates',
row_estimate_method 'showplan_all' );
DROP TABLE IF EXISTS tmp_dates;
CREATE TEMP TABLE tmp_dates (
ID integer NOT NULL,
Val date NULL );
INSERT INTO tmp_dates (ID, Val)
SELECT ID, regexp_replace(Val,' ..:..:..:..','')::date
FROM tds_test_tmp_dates;
SELECT * FROM tmp_dates; /*
1 2000-01-01
2 NULL */
Комментарии (12)
Akina
16.11.2023 23:43А в возможность последовательной вставки более миллиона записей в секунду в одну таблицу MS SQL верится с трудом.
Не говорите ерунды. Если поле определено как column DATETIME2(7) DEFAULT (SYSDATETIME()), то все записи, вставленные одним запросом без явного присвоения значения в это поле, получат одно и то же значение (емнип значение штампа времени начала выполнения запроса). А если это INSERT .. SELECT, то сколько SELECT вернёт, столько и вставится. Хоть миллион, хоть миллиард. И у всех будет одно и то же значение штампа времени - с точностью до последней цифры, вне зависимости от того, сколько времени потребовало выполнение запроса.
См. напр. https://dbfiddle.uk/vg3LIzfG
ptr128 Автор
16.11.2023 23:43+1"Не говорите ерунды." (с)
В этом случае их вставка не была последовательной, и их взаимная сортировка не имела и не имеет значение. То есть это ни случай "точность до 100 наносекунд жизненно необходима", ни, тем более, случай "сохранения порядка сортировки", к котором относилась выдернутая Вами из контекста процитированная фраза.Akina
16.11.2023 23:43Вставка выполняется именно что последовательно. Можете убедиться - выполните вставку набора записей в таблицу с ограничением уникальности, где среди вставляемых записей присутствуют дубликаты - ошибка выполнения возникнет именно на второй записи-дубликате, тогда как при одновременной вставке ограничение уникальности должно быть обнаружено либо до фактический вставки, либо в момент её выполнения, т.е. на первой записи-дубликате.
Если бы существовала функция, возвращающая штамп времени вставки, а не начала выполнения запроса (как это в MySQL делает функция SYSDATE()), это можно было бы увидеть. Если бы вместо "последовательной вставки" было написано "вставки разными/отдельными запросами" - слова бы не сказал..
ptr128 Автор
16.11.2023 23:43Вставка выполняется именно что последовательно.
Откуда Вы это взяли? Пруф? В рамках одного INSERT вставка записей в таблицу может выполняться сервером не только в произвольном порядке, но и несколькими процессами параллельно. Даже явно указанный ORDER BY, при отсутствии кластерного индекса в таблице MS SQL, влияет только на сортировку записей внутри страниц. Никогда такого не видели?
И опять, какое отношение это имеет к сохранению порядка сортировки?
para_u
16.11.2023 23:43занятная заметка. трансформация типов данных между разными частями приложения знатная боль, а тут опыт миграции между специфическими типами разных систем.
у меня не большой опыт. натыкаясь на подобные статьи у меня возникает вопрос: А стоит ли настолько сильно приростать к конкретной СУБД/фреймворку, используя уникальные типы данных или наращивая метаструктуру, снижая поддерживаемость конечного ПО? неужели профит настолько ценнен, чтобы проглатывать горсть синтаксического сахара, а потом потеть на миграциях и конвертациях данных и логики?
ptr128 Автор
16.11.2023 23:43Уникальные типы данных MS SQL (Spatial Geometry/Geography или hierarchyid) я не рассматривал вообще именно по озвученной Вами причине. А money и datetime2 - как раз полностью соответствуют требованиям ANSI/ISO 8601, в отличии от того же datetime. Но, как обычно, дъявол кроется в деталях.
AndrewJD
datetime2 - это не про вставку, а про хранение данных с высокой точностью. Временные точки могут приходить из приложения где микросекунды - это уже много.
ptr128 Автор
Для такого случая я указал: "Если вдруг точность до 100 наносекунд жизненно необходима, то в PostgreSQL ее потребуется поддерживать отдельным полем типа smallint."
А то, что Вы процитровали, относится уже к "модификации значений типа datetime2 таким образом, чтобы они стали различаться в пределах шести знаков после десятичной точки для сохранения порядка сортировки"
Просто интересно, что за приложение? И как так получилось, что 100 наносекунд достаточно, а 1 микросекунда - уже много?
qw1
Внутри datetime2 - это int64, может и хранить в таком типе, вместо того, чтобы извращаться с двумя столбцами. Интерпретировать только на клиенте. Потому что превратить в timestamp без потери точности - нельзя.
ptr128 Автор
Двухстолбцовая реализация позволяет легко интерпретировать данные в хранимых процедурах, функциях, триггерах и запросах, а не только на клиенте.
Если уж совсем невмоготу - создавайте лучше свой базовый тип для этой цели.
Что у Вас за приложение, что не для целочисленных интервалов времени, а именно для дат 100 наносекунд достаточно, а 1 микросекунда - уже много?
qw1
Это ваш пример "если жизненно необходимо". Я лишь возражаю, что решение с доп. столбцом smallint - супер-кривое (например, при сортировках производительноть будет хуже, во всех переливалках надо будет лить через 2 переменные и т.д.). Если нужна интерпретируемость, элементарно делается UDF, которая приводит int64 к дате просто умножением на некоторую константу и прибавлением к базовой дате, с которой начинается календарь.
ptr128 Автор
У меня его нет. Я же писал: "Такого в своей практике не встречал".
Так про то и речь, что издержки на UDF в итоге оказываются выше, чем выигрыш от этих 16 бит при сортировке.