(SQL) SQL (2010 год)

SQL-проектирование в PostgreSQL. Над плоским миром MS SQL.

У MS SQL столько раздражающих особенностей, что о них можно написать тысячи заметок. Одно только, что каждый отдельный функционал MS SQL требует отдельных денег - чего стоит! Например, если у вас база в влазит в 10GB - можно пользовться SQL Express, но если вдруг вы хотите иметь более ли менее актуальную копию своей базы (а это обычно поднимается на LogShiping) или иметь SQL-задания - тогда будьте любезны доплатить Биллу Гейтсу по $3500 на процессор, а если вдруг вам понадобился Профайлер - будьте любезны заплатить по $3700 на процессор, а если нужна интеграция с почтовиком или Export/Import Wizard - заплатите по $7100 на процессор, а если вдруг понадобилось сжатие бекапов или аудит - заплатите по $25000 за процессор.

А микрософтовский клиент для программирования в MS SQL (MS SMS) у меня вызывает настолько рвотный рефлекс, что я даже написал свой собственный альтернативный клиент для программирования в MS SQL - ScriptManager - Менеджер MS SQL сервера - дополнение MS SMS для работы с большими скриптами. Это оказалось проще чем травмировать свою психику ежедневно сталкиваясь с тупостью микрософтовского MS SMS.


Мне бы хотелось проанализировать некоторые возможности PostgreSQL, предоставляемые SQL-разработчику - о которых и не подозревает разработчик в плоском мире MS SQL. Я уже написал о некоторых взглядах на SQL-проектирования в PostgreSQL несколько заметок, например Выполняем разворот строк в столбцы в MS SQL и PostgreSQL, Пример обьектно-реляционного проектирования структуры данных в PostgreSQL.


Но на этой страничке мне бы хотелось остановиться лишь на одной-единственной особенности проектирования в MS SQL - благодаря которой MS SQL проектировщики обречены на вечный копипаст и бесконечные повторы своего кода - вместо вызова уже написанного кода из своих новых процедур.


Основная идея плоского мира MS SQL (только держите крепче PostgreSQL-проектировщиков - когда они узнают об этом они со стула упадут) - то что в новых Transact-SQL процедурах, Transact-SQL вьюшках и Transact-SQL функциях НЕЛЬЗЯ использовать результат, полученный в ранее спроектированных и отлаженных Transact-SQL процедурах.

Это кажется совершенно невероятным, но в MS SQL действительно практически невозможно построить процедуру, функцию или вьюшку, использующую некий ранее сформированный результат. Собственно говоря, в MS SQL существует три лазейки:

Итак, обычный код Transact-SQL в самом же Transact-SQL вызвать нельзя. А в обычной MS SQL-системе 99% кода находится к теле Transact-SQL процедур. MS SQL-функции (собственно то что можно вызывать) имеет адские ограничения. Либо табличная функция должны быть в один оператор, либо если в несколько - то тогда можно вернуть только один простой скалярный тип. Либо что-то можно выкрутить на сборках, что работает только в самой дорогой версии MS SQL и требует совершенно иных навыков программирования и администрирования, чем обычное SQL-программирование.

Поэтому MS SQL проектировщик обречен на вечный копипаст ранее отлаженных фрагментов своего кода из одной Transact-SQL процедуры в другую.


В отличие от плоского мира MS SQL - PostgreSQL позволяет вести классическое поэтапное иерархическое проектирование с использованием в новых слоях кода ранее наработанных функций. Например, в этом месяце делаем функции самого нижнего уровня, в следующем месяце - вызывая готовый отлаженный код - создаем следующий SQL-уровень. Ведь в PostgreSQL ЛЮБОЙ созданный ранее plpgsql и SQL-код можно вызвать в ЛЮБОМ PostgreSQL-коде.

В PostgreSQL функции могут возвращать:

Одно из удивительных последствий такой гибкости PostgreSQL-девелопмента - порядок создания SQL-структуры, когда ее надо развернуть одним последовательным скриптом с нуля, например в боевой среде. В MS SQL всегда есть жесткая последовательность - сначала создаем все таблы, потом контрейнсы, потом вьюхи, потом функции, потом процедуры. В postgreSQL последовательность получается совершенно удивительная. Превыми создаются всегда последовательности, потом создатся таблы нижнего слоя на простых типах, потом какие-то функции, которые формируют основные типы нижнего слоя софта, потом вьюхи и таблы на этих типах - потом следующий слой софта - и так до самых верхних слоев софта. Фактически последовательность развертывания SQL-структуры отражает последовательность разработки. Но если ошибка допущена в начале PostgreSQL-разработки или задача была изначально поставлена неточно - мало не покажется!


Попробую проиллюстрировать сказанное каким нибудь доступным примером. На неком среднем уровне софта у меня есть вьюшка GET_СкладскиеОстаткиСоСвойствами, которая основана на трех других вьюшках:

   1:  CREATE OR REPLACE VIEW "_Delmar"."GET_СкладскиеОстаткиСоСвойствами" AS 
   2:   SELECT "GET_СкладскиеОстатки".i, 
   3:      "GET_СкладскиеОстатки".toimportsclad, 
   4:      "GET_СкладскиеОстатки"."ПредложениеИд", 
   5:      "GET_СкладскиеОстатки"."ПредложениеАртикул",
   6:      "GET_СкладскиеОстатки"."ПредложениеНаименование", 
   7:      "GET_СкладскиеОстатки"."ПредложениеБазоваяЕдиница", 
   8:      "GET_СкладскиеОстатки"."ПредложениеБазоваяЕдиницаКод", 
   9:      "GET_СкладскиеОстатки"."ПредложениеБазоваяЕдиницаНаимен", 
  10:      "GET_СкладскиеОстатки"."ПредложениеБазоваяМеждународное", 
  11:      "GET_СкладскиеОстатки"."ПредложениеЦена", 
  12:      "GET_СкладскиеОстатки"."ПредложениеЦенаИдТипаЦены", 
  13:      "GET_СкладскиеОстатки"."ПредложениеЦенаЗаЕдиницу", 
  14:      "GET_СкладскиеОстатки"."ПредложениеЦенаВалюта", 
  15:      "GET_СкладскиеОстатки"."ПредложениеЦенаЕдиница", 
  16:      "GET_СкладскиеОстатки"."ПредложениеЦенаКоэффициент", 
  17:      "GET_СкладскиеОстатки"."Остаток", 
  18:      "GET_СкладскиеОстатки"."ОстатокИдСклада", 
  19:      "GET_СкладскиеОстатки"."ОстатокKol", 
  20:          CASE
  21:              WHEN "substring"("GET_СкладскиеОстатки"."ОстатокKol"::text, '^[[:digit:]]*'::text) = ''::text THEN 0::numeric
  22:              ELSE "substring"("GET_СкладскиеОстатки"."ОстатокKol"::text, '^[[:digit:]]*'::text)::numeric
  23:          END AS "ОстатокЧисловой", "GET_СкладскиеОстатки"."ОстатокЕдиница", "GET_СкладскиеОстатки"."ОстатокКоэффициент", "GET_СкладскиеОстатки"."ИмпортСкладскихОстатков_i", "GET_СкладскиеОстатки"."ИмпортСкладскихОстатков_ДатаИмпо", "GET_ВсеСвойстваТовара".totovar, "GET_ВсеСвойстваТовара"."Вылет - ET (мм)", "GET_ВсеСвойстваТовара"."Индекс нагрузки шины", "GET_ВсеСвойстваТовара"."Индекс скорости шины", "GET_ВсеСвойстваТовара"."Посадочный диаметр шины (дюйм)", "GET_ВсеСвойстваТовара"."Размер диска", 
  24:          CASE
  25:              WHEN "substring"("GET_ВсеСвойстваТовара"."Размер диска"::text, '^[[:digit:]]*'::text) = ''::text THEN '0'::character varying
  26:              ELSE "substring"("GET_ВсеСвойстваТовара"."Размер диска"::text, '^[[:digit:]]*'::text)::character varying(10)
  27:          END AS "ДиаметрДиска", 
  28:          CASE
  29:              WHEN "GET_ВсеСвойстваТовара"."Размер диска"::text = ''::text THEN '0'::character varying
  30:              ELSE "substring"("GET_ВсеСвойстваТовара"."Размер диска"::text, "position"("GET_ВсеСвойстваТовара"."Размер диска"::text, '*'::text) + 1)::character varying(10)
  31:          END AS "ШиринаДиска", "GET_ВсеСвойстваТовара"."Размер шины", "GET_ВсеСвойстваТовара"."Сверловка", 
  32:          CASE
  33:              WHEN "substring"("GET_ВсеСвойстваТовара"."Сверловка"::text, '^[[:digit:]]*'::text) = ''::text THEN '0'::character varying
  34:              ELSE "substring"("GET_ВсеСвойстваТовара"."Сверловка"::text, '^[[:digit:]]*'::text)::character varying(10)
  35:          END AS "Сверловка-HOLE", 
  36:          CASE
  37:              WHEN "substring"("GET_ВсеСвойстваТовара"."Сверловка"::text, '^[[:digit:]]*'::text) = ''::text THEN '0'::character varying
  38:              ELSE "substring"("GET_ВсеСвойстваТовара"."Сверловка"::text, "position"("GET_ВсеСвойстваТовара"."Сверловка"::text, '/'::text) + 1)::character varying(10)
  39:          END AS "Сверловка-PCD", "GET_ВсеСвойстваТовара"."Сезонность шины", "GET_ВсеСвойстваТовара"."Тип шины", "GET_ВсеСвойстваТовара"."Цвет", 
  40:          CASE
  41:              WHEN "substring"("GET_ВсеСвойстваТовара"."Центрально отверстие - DIA (мм)"::text, '^[[:digit:]]*'::text) = ''::text THEN '0'::character varying
  42:              ELSE "substring"("GET_ВсеСвойстваТовара"."Центрально отверстие - DIA (мм)"::text, '^[[:digit:]]*'::text)::character varying(10)
  43:          END AS "Центрально отверстие - DIA (мм)", "GET_ВсеСвойстваТовара"."Шина повышенной проходимости (M+S)", "GET_ВсеСвойстваТовара"."Шина усиленная (C)", "GET_ВсеСвойстваТовара"."Шипованная шина", "GET_ТоварныйКлассификатор"."ТоварИд", "GET_ТоварныйКлассификатор"."ТоварКартинка"
  44:     FROM "_Delmar"."GET_СкладскиеОстатки"
  45:     JOIN "_Delmar"."GET_ВсеСвойстваТовара" ON "GET_СкладскиеОстатки".i = "GET_ВсеСвойстваТовара".totovar
  46:     JOIN "_Delmar"."GET_ТоварныйКлассификатор" ON "GET_ТоварныйКлассификатор"."ТоварАртикул"::text = "GET_СкладскиеОстатки"."ПредложениеАртикул"::text;
  47:   
  48:  ALTER TABLE "_Delmar"."GET_СкладскиеОстаткиСоСвойствами" OWNER TO postgres;

Она искользуется много раз в разных местах. И в том числе некоторой перегруженной функцией DiskCount, которая позволяет отобрать из вьюшки GET_СкладскиеОстаткиСоСвойствами диски по более ли менее точно заданным параметрам. Либо вообще все, либо только по размеру 18", либо по размеру и ширине - и так далее до точного отбора по шести параметрам:


   1:  CREATE OR REPLACE FUNCTION "DiskCount"
   2:  (
   3:  "ДиаметрДиска" character varying, 
   4:  "ШиринаДиска" character varying, 
   5:  "Вылет_ET" character varying, 
   6:  "Сверловка-HOLE" character varying, 
   7:  "Сверловка-PCD" character varying, 
   8:  "ЦентральноеОтверстие_DIA" character varying
   9:  ) RETURNS integer
  10:  LANGUAGE plpgsql  AS $_$
  11:  DECLARE res int;
  12:  BEGIN 
  13:   SELECT count(*) INTO res FROM "GET_СкладскиеОстаткиСоСвойствами"
  14:   WHERE "ОстатокЧисловой">4 
  15:    and $1="GET_СкладскиеОстаткиСоСвойствами"."ДиаметрДиска"
  16:    and REPLACE($2,',','.')::real+0.5>= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."ШиринаДиска",',','.')::real
  17:    and REPLACE($2,',','.')::real-0.5<= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."ШиринаДиска",',','.')::real
  18:    and REPLACE($3,',','.')::real>= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."Вылет - ET (мм)",',','.')::real-10
  19:    and REPLACE($3,',','.')::real<= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."Вылет - ET (мм)",',','.')::real+2
  20:    and REPLACE($4,',','.')= "GET_СкладскиеОстаткиСоСвойствами"."Сверловка-HOLE"
  21:    and REPLACE($5,',','.')= "GET_СкладскиеОстаткиСоСвойствами"."Сверловка-PCD"
  22:    and REPLACE($6,',','.')<= "GET_СкладскиеОстаткиСоСвойствами"."Центрально отверстие - DIA (мм)" ;
  23:   RETURN res;
  24:   
  25:  EXCEPTION
  26:      when others then
  27:      insert into "TerminalError"(crdate,txt) values (now(), 'DiskCount : ' || SQLSTATE || ' : ' || SQLERRM) ;
  28:      return -1;
  29:  END;
  30:  $_$;
  31:   
  32:   
  33:  CREATE OR REPLACE FUNCTION "DiskCount"
  34:  (
  35:  "ДиаметрДиска" character varying, 
  36:  "ШиринаДиска" character varying, 
  37:  "Вылет_ET" character varying, 
  38:  "Сверловка-HOLE" character varying, 
  39:  "Сверловка-PCD" character varying
  40:  ) RETURNS integer
  41:  LANGUAGE plpgsql AS $_$
  42:  DECLARE res int;
  43:  BEGIN 
  44:   SELECT count(*) INTO res FROM "GET_СкладскиеОстаткиСоСвойствами"
  45:   WHERE "ОстатокЧисловой">4 
  46:    and $1="GET_СкладскиеОстаткиСоСвойствами"."ДиаметрДиска"
  47:    and REPLACE($2,',','.')::real+0.5>= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."ШиринаДиска",',','.')::real
  48:    and REPLACE($2,',','.')::real-0.5<= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."ШиринаДиска",',','.')::real
  49:    and REPLACE($3,',','.')::real>= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."Вылет - ET (мм)",',','.')::real-10
  50:    and REPLACE($3,',','.')::real<= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."Вылет - ET (мм)",',','.')::real+2
  51:    and REPLACE($4,',','.')= "GET_СкладскиеОстаткиСоСвойствами"."Сверловка-HOLE"
  52:    and REPLACE($5,',','.')= "GET_СкладскиеОстаткиСоСвойствами"."Сверловка-PCD"
  53:    ;
  54:   RETURN res;
  55:   EXCEPTION
  56:      when others then
  57:      insert into "TerminalError"(crdate,txt) values (now(), 'DiskCount : ' || SQLSTATE || ' : ' || SQLERRM) ;
  58:      return -1;
  59:  END;
  60:  $_$;
  61:   
  62:   
  63:  CREATE OR REPLACE FUNCTION "DiskCount"
  64:  (
  65:  "ДиаметрДиска" character varying, 
  66:  "ШиринаДиска" character varying, 
  67:  "Вылет_ET" character varying, 
  68:  "Сверловка-HOLE" character varying
  69:  ) RETURNS integer
  70:  LANGUAGE plpgsql AS $_$
  71:  DECLARE res int;
  72:  BEGIN 
  73:   SELECT count(*) INTO res FROM "GET_СкладскиеОстаткиСоСвойствами"
  74:   WHERE "ОстатокЧисловой">4 
  75:    and $1="GET_СкладскиеОстаткиСоСвойствами"."ДиаметрДиска"
  76:    and REPLACE($2,',','.')::real+0.5>= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."ШиринаДиска",',','.')::real
  77:    and REPLACE($2,',','.')::real-0.5<= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."ШиринаДиска",',','.')::real
  78:    and REPLACE($3,',','.')::real>= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."Вылет - ET (мм)",',','.')::real-10
  79:    and REPLACE($3,',','.')::real<= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."Вылет - ET (мм)",',','.')::real+2
  80:    and REPLACE($4,',','.')= "GET_СкладскиеОстаткиСоСвойствами"."Сверловка-HOLE"
  81:    ;
  82:   RETURN res;
  83:   EXCEPTION
  84:      when others then
  85:      insert into "TerminalError"(crdate,txt) values (now(), 'DiskCount : ' || SQLSTATE || ' : ' || SQLERRM) ;
  86:      return -1;
  87:  END;
  88:  $_$;
  89:   
  90:   
  91:  CREATE OR REPLACE FUNCTION "DiskCount"
  92:  (
  93:  "ДиаметрДиска" character varying, 
  94:  "ШиринаДиска" character varying, 
  95:  "Вылет_ET" character varying
  96:  ) RETURNS integer
  97:  LANGUAGE plpgsql AS $_$
  98:  DECLARE res int;
  99:  BEGIN 
 100:   SELECT count(*) INTO res FROM "GET_СкладскиеОстаткиСоСвойствами"
 101:   WHERE "ОстатокЧисловой">4 
 102:    and $1="GET_СкладскиеОстаткиСоСвойствами"."ДиаметрДиска"
 103:    and REPLACE($2,',','.')::real+0.5>= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."ШиринаДиска",',','.')::real
 104:    and REPLACE($2,',','.')::real-0.5<= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."ШиринаДиска",',','.')::real
 105:    and REPLACE($3,',','.')::real>= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."Вылет - ET (мм)",',','.')::real-10
 106:    and REPLACE($3,',','.')::real<= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."Вылет - ET (мм)",',','.')::real+2
 107:    ;
 108:   RETURN res;
 109:   EXCEPTION
 110:      when others then
 111:      Insert into "TerminalError"(crdate,txt) values (now(), 'DiskCount : ' || SQLSTATE || ' : ' || SQLERRM) ;
 112:      return -1;
 113:  END;
 114:  $_$;
 115:   
 116:   
 117:  CREATE OR REPLACE FUNCTION "DiskCount"
 118:  (
 119:  "ДиаметрДиска" character varying, 
 120:  "ШиринаДиска" character varying
 121:  ) RETURNS integer
 122:  LANGUAGE plpgsql AS $_$
 123:  DECLARE res int;
 124:  BEGIN 
 125:   SELECT count(*) INTO res FROM "GET_СкладскиеОстаткиСоСвойствами"
 126:   WHERE "ОстатокЧисловой">4 
 127:    and REPLACE($1,',','.')="GET_СкладскиеОстаткиСоСвойствами"."ДиаметрДиска"
 128:    and REPLACE($2,',','.')::real+0.5>= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."ШиринаДиска",',','.')::real
 129:    and REPLACE($2,',','.')::real-0.5<= REPLACE("GET_СкладскиеОстаткиСоСвойствами"."ШиринаДиска",',','.')::real
 130:    ;
 131:   RETURN res;
 132:   EXCEPTION
 133:      when others then
 134:      Insert into "TerminalError"(crdate,txt) values (now(), 'DiskCount : ' || SQLSTATE || ' : ' || SQLERRM) ;
 135:      return -1;
 136:  END;
 137:  $_$;
 138:   
 139:   
 140:  CREATE OR REPLACE FUNCTION "DiskCount"
 141:  (
 142:  "ДиаметрДиска" character varying
 143:  ) RETURNS integer
 144:  LANGUAGE plpgsql AS $_$
 145:  DECLARE res int;
 146:  BEGIN 
 147:   SELECT count(*) INTO res FROM "GET_СкладскиеОстаткиСоСвойствами"
 148:   WHERE "ОстатокЧисловой">4 
 149:    and REPLACE($1,',','.')="GET_СкладскиеОстаткиСоСвойствами"."ДиаметрДиска"
 150:    ;
 151:   RETURN res;
 152:   EXCEPTION
 153:      when others then
 154:      Insert into "TerminalError"(crdate,txt) values (now(), 'DiskCount : ' || SQLSTATE || ' : ' || SQLERRM) ;
 155:      return -1;
 156:  END;
 157:  $_$;
 158:   
 159:   
 160:  CREATE OR REPLACE FUNCTION "DiskCount"() RETURNS integer
 161:      LANGUAGE plpgsql
 162:      AS $_$
 163:  DECLARE res int;
 164:  BEGIN 
 165:   SELECT count(*) INTO res FROM "GET_СкладскиеОстаткиСоСвойствами"
 166:   WHERE "ОстатокЧисловой">4   ;
 167:   RETURN res;
 168:   EXCEPTION
 169:      when others then
 170:      Insert into "TerminalError"(crdate,txt) values (now(), 'DiskCount : ' || SQLSTATE || ' : ' || SQLERRM) ;
 171:      return -1;
 172:  END;
 173:  $_$;
 174:   

В следующем сое софта над этой функцией есть вьюшка AllDiskCount, которая даже не знает о том, что эта функция перегружена. Она просто использует код функции DiskCount.


   1:  CREATE OR REPLACE VIEW "AllDiskCount" AS
   2:      SELECT "DiskCount"('12'::character varying) AS "12", 
   3:             "DiskCount"('13'::character varying) AS "13", 
   4:             "DiskCount"('14'::character varying) AS "14", 
   5:             "DiskCount"('15'::character varying) AS "15", 
   6:             "DiskCount"('16'::character varying) AS "16", 
   7:             "DiskCount"('17'::character varying) AS "17", 
   8:             "DiskCount"('18'::character varying) AS "18";

В следующем слое софта есть таблица, которая построена на основе типа AllDiskCount и еще шести типах:


   1:  CREATE TABLE "TerminalMonitor" (
   2:      i integer NOT NULL,
   3:      crdate timestamp without time zone,
   4:      "TerminalID" uuid NOT NULL,
   5:      "Data" timestamp without time zone,
   6:      "ИмпортПрименяемости" "ИмпортПрименяемостиCount",
   7:      "ИмпортСкладскихОстатков" "ИмпортСкладскихОстатковCount",
   8:      "ИмпортТоваров" "ИмпортТоваровCount",
   9:      "TableCount" "TableCount",
  10:      "AllDiskCount" "AllDiskCount",
  11:      "ЗаказыCount" "ЗаказыCount",
  12:      "ErrorCount" "ErrorCount"
  13:  );

В следующем слое софта есть функция LocalMonitoring, которая использует таблицу TerminalMonitor, которая построена на основе типа AllDiskCount (и шести других сложных типах), которая в свою очередь построена на функции GetDisk, построенной на отборах из вьюшки GET_СкладскиеОстаткиСоСвойствами, которая в свою очередь построена еще на трех других вьюшках... И это всего лишь некий промежуточный слой из огромного многослойного пирога.


   1:   
   2:  CREATE OR REPLACE FUNCTION "LocalMonitoring"() RETURNS bigint
   3:  LANGUAGE sql AS $$
   4:  insert into "TerminalMonitor" 
   5:    (crdate, 
   6:    "TerminalID", 
   7:    "Data", 
   8:    "ИмпортПрименяемости",
   9:    "ИмпортСкладскихОстатков", 
  10:    "ИмпортТоваров", 
  11:    "TableCount", 
  12:    "AllDiskCount", 
  13:    "ЗаказыCount", 
  14:    "ErrorCount")
  15:  Select now(),
  16:    "TerminalID",
  17:    now(),
  18:    "ИмпортПрименяемости", 
  19:    "ИмпортСкладскихОстатков", 
  20:    "ИмпортТоваров", 
  21:    "TableCount", 
  22:    "AllDiskCount", 
  23:    "ЗаказыCount", 
  24:    "ErrorCount"
  25:  from "CurrentState";
  26:  select currval('"TerminalMonitor_i_seq"'::regclass)
  27:  $$;

Вы где нибудь видели такое в MS SQL? Где 99,99% кода утоплено в тело Transact-SQL процедур, которые вообще никак нельзя вызвать из другой процедуры.


Узнать о PostgreSQL больше - вы можете здесь - Используем PostgreSQL вместо MS SQL в проектах на NET и ASP.NET

Comments ( )
Link to this page: //www.vb-net.com/PostgreSQL_hierarchy/index.htm
< THANKS ME>