Исходя из того, что предыдущую статью не заминусовали и даже не сильно критиковали, попробую продолжить серию и поделиться с проблемами некоторых различий типов данных в 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)


  1. AndrewJD
    16.11.2023 23:43

    в возможность последовательной вставки более миллиона записей в секунду в одну таблицу MS SQL верится с трудом

    datetime2 - это не про вставку, а про хранение данных с высокой точностью. Временные точки могут приходить из приложения где микросекунды - это уже много.


    1. ptr128 Автор
      16.11.2023 23:43

      Для такого случая я указал: "Если вдруг точность до 100 наносекунд жизненно необходима, то в PostgreSQL ее потребуется поддерживать отдельным полем типа smallint."

      А то, что Вы процитровали, относится уже к "модификации значений типа datetime2 таким образом, чтобы они стали различаться в пределах шести знаков после десятичной точки для сохранения порядка сортировки"

      из приложения где микросекунды - это уже много

      Просто интересно, что за приложение? И как так получилось, что 100 наносекунд достаточно, а 1 микросекунда - уже много?


      1. qw1
        16.11.2023 23:43
        +2

        Внутри datetime2 - это int64, может и хранить в таком типе, вместо того, чтобы извращаться с двумя столбцами. Интерпретировать только на клиенте. Потому что превратить в timestamp без потери точности - нельзя.


        1. ptr128 Автор
          16.11.2023 23:43

          Двухстолбцовая реализация позволяет легко интерпретировать данные в хранимых процедурах, функциях, триггерах и запросах, а не только на клиенте.

          Если уж совсем невмоготу - создавайте лучше свой базовый тип для этой цели.

          Что у Вас за приложение, что не для целочисленных интервалов времени, а именно для дат 100 наносекунд достаточно, а 1 микросекунда - уже много?


          1. qw1
            16.11.2023 23:43

            Это ваш пример "если жизненно необходимо". Я лишь возражаю, что решение с доп. столбцом smallint - супер-кривое (например, при сортировках производительноть будет хуже, во всех переливалках надо будет лить через 2 переменные и т.д.). Если нужна интерпретируемость, элементарно делается UDF, которая приводит int64 к дате просто умножением на некоторую константу и прибавлением к базовой дате, с которой начинается календарь.


            1. ptr128 Автор
              16.11.2023 23:43

              Это ваш пример "если жизненно необходимо".

              У меня его нет. Я же писал: "Такого в своей практике не встречал".

              например, при сортировках производительноть будет хуже

              делается UDF

              Так про то и речь, что издержки на UDF в итоге оказываются выше, чем выигрыш от этих 16 бит при сортировке.

              CREATE OR REPLACE FUNCTION bigint2ts(bigint)
                RETURNS timestamp
                LANGUAGE SQL AS $func$
                SELECT '1900-01-01'::timestamp+($1/10)*'1 microsecond'::interval;
              $func$;
              
              DROP TABLE IF EXISTS tmp_tmp;
              CREATE TABLE tmp_tmp AS
              SELECT clock_timestamp() AS ts_val,
                G.Id::bigint AS bigint_val,
                (G.Id%10)::smallint AS hundred_nanos
              FROM generate_series(1,10000000) G(Id);
              
              DROP TABLE IF EXISTS tmp_sorted;
              EXPLAIN ANALYZE
              CREATE TEMP TABLE tmp_sorted AS
              SELECT ts_val, bigint_val, hundred_nanos
              FROM tmp_tmp
              ORDER BY ts_val DESC, hundred_nanos DESC;
              -- Planning Time: 0.039 ms
              -- Execution Time: 4920.576 ms
              
              DROP TABLE IF EXISTS tmp_sorted;
              EXPLAIN ANALYZE
              CREATE TEMP TABLE tmp_sorted AS
              SELECT ts_val, bigint2ts(bigint_val) AS calc_ts, hundred_nanos
              FROM tmp_tmp
              ORDER BY bigint_val DESC;
              -- Planning Time: 0.086 ms
              -- Execution Time: 5547.087 ms


  1. Akina
    16.11.2023 23:43

    А в возможность последовательной вставки более миллиона записей в секунду в одну таблицу MS SQL верится с трудом.

    Не говорите ерунды. Если поле определено как column DATETIME2(7) DEFAULT (SYSDATETIME()), то все записи, вставленные одним запросом без явного присвоения значения в это поле, получат одно и то же значение (емнип значение штампа времени начала выполнения запроса). А если это INSERT .. SELECT, то сколько SELECT вернёт, столько и вставится. Хоть миллион, хоть миллиард. И у всех будет одно и то же значение штампа времени - с точностью до последней цифры, вне зависимости от того, сколько времени потребовало выполнение запроса.

    См. напр. https://dbfiddle.uk/vg3LIzfG


    1. ptr128 Автор
      16.11.2023 23:43
      +1

      "Не говорите ерунды." (с)
      В этом случае их вставка не была последовательной, и их взаимная сортировка не имела и не имеет значение. То есть это ни случай "точность до 100 наносекунд жизненно необходима", ни, тем более, случай "сохранения порядка сортировки", к котором относилась выдернутая Вами из контекста процитированная фраза.


      1. Akina
        16.11.2023 23:43

        Вставка выполняется именно что последовательно. Можете убедиться - выполните вставку набора записей в таблицу с ограничением уникальности, где среди вставляемых записей присутствуют дубликаты - ошибка выполнения возникнет именно на второй записи-дубликате, тогда как при одновременной вставке ограничение уникальности должно быть обнаружено либо до фактический вставки, либо в момент её выполнения, т.е. на первой записи-дубликате.

        Если бы существовала функция, возвращающая штамп времени вставки, а не начала выполнения запроса (как это в MySQL делает функция SYSDATE()), это можно было бы увидеть. Если бы вместо "последовательной вставки" было написано "вставки разными/отдельными запросами" - слова бы не сказал..


        1. ptr128 Автор
          16.11.2023 23:43

          Вставка выполняется именно что последовательно.

          Откуда Вы это взяли? Пруф? В рамках одного INSERT вставка записей в таблицу может выполняться сервером не только в произвольном порядке, но и несколькими процессами параллельно. Даже явно указанный ORDER BY, при отсутствии кластерного индекса в таблице MS SQL, влияет только на сортировку записей внутри страниц. Никогда такого не видели?

          И опять, какое отношение это имеет к сохранению порядка сортировки?


  1. para_u
    16.11.2023 23:43

    занятная заметка. трансформация типов данных между разными частями приложения знатная боль, а тут опыт миграции между специфическими типами разных систем.

    у меня не большой опыт. натыкаясь на подобные статьи у меня возникает вопрос: А стоит ли настолько сильно приростать к конкретной СУБД/фреймворку, используя уникальные типы данных или наращивая метаструктуру, снижая поддерживаемость конечного ПО? неужели профит настолько ценнен, чтобы проглатывать горсть синтаксического сахара, а потом потеть на миграциях и конвертациях данных и логики?


    1. ptr128 Автор
      16.11.2023 23:43

      Уникальные типы данных MS SQL (Spatial Geometry/Geography или hierarchyid) я не рассматривал вообще именно по озвученной Вами причине. А money и datetime2 - как раз полностью соответствуют требованиям ANSI/ISO 8601, в отличии от того же datetime. Но, как обычно, дъявол кроется в деталях.