选择行单元格作为新列

时间:2012-05-25 09:19:43

标签: sql postgresql plpgsql dynamic-sql crosstab

基本上,我有一个表存储具有一些限制的列名:infos,另一个存储这些列的值:info_data。我想要一个包含infos列和info_data数据的表格。我尝试过使用交叉表功能,但它没有达到预期的效果。

我有两张桌子:

CREATE TABLE infos
(id serial PRIMARY KEY,
 name text NOT NULL,
 id_member integer NOT NULL,
 title text,
 min_length integer NOT NULL DEFAULT 0,
 max_length integer NOT NULL DEFAULT 30,
 required boolean NOT NULL DEFAULT false,
 type text NOT NULL DEFAULT 'text'::text
);

CREATE INDEX info_id_idx ON infos (id);

CREATE TABLE info_data
(id serial PRIMARY KEY,
 id_info integer,
 value text,
  CONSTRAINT info_data_id_info_fkey FOREIGN KEY (id_info)
      REFERENCES infos (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
);

CREATE INDEX info_data_id_idx ON info_data(id);

具有以下值:

的相关信息:

COPY infos (id, name, id_member, title, min_length, max_length, required, type)     FROM     stdin;
1 nume 1 Nume 0 30 t text
2 prenume 1 Prenume 0 30 t text
3 cnp 1 C.N.P. 13 13 t number
4 nume anterior 1 Nume anterior 0 30 f text
5 stare civila 1 Starea civila 0 30 f text
6 cetatenie 1 Cetatenie 0 30 f text
7 rezidenta 1 Rezidenta 0 30 f text
9 tip act 1 C.I. / B.I. 0 10 t text
10 serie ci 1 Serie C.I. / B.I. 0 30 t text
11 numar ci 1 Numar C.I. / B.I. 0 30 t text
12 data eliberarii 1 Data eliberarii 0 30 t text
13 eliberat de 1 Eliberat de 0 30 t text
8 adresa 1 Adresa 0 50 f text
\.

info_data:

COPY info_data (id, id_info, value) FROM stdin;
1 1 a
2 2 a
3 3 100
4 4
5 5
6 6
7 7
8 8
9 9 ci
10 10 sv
11 11 13
12 12 132
13 13 123
14 1 b
15 2 b
16 3 100
17 4
18 5
19 6
20 7
21 8
22 9 BI
23 10 XT
24 11 123
25 12 10
26 13 10
\.

问题: 我怎样才能实现这个输出? (必须根据infos

中的唯一条目生成列
nume, prenume, cnp, nume anterior, ... (as columns - built from infos)
a   , a, ...
b   , b, ... (as rows - built from info_data)

4 个答案:

答案 0 :(得分:1)

这个问题要比你想象的要困难得多。您crosstab()的尝试是针对正确的方向。但是要分配动态列名,还需要动态SQL:EXECUTE in a plpgsql function.

将列infos.type的数据类型从text更改为regtype,以防止SQL注入和其他错误。例如,您具有数据类型number,它不是有效的PostgreSQL数据类型。我用numeric替换它,所以它可以工作。

可以通过避免需要双引号的列名来简化任务。与nume_anterior代替"nume anterior"一样。

您可能希望在表row_id中添加列info_data以标记一行的所有元素。您需要crosstab()函数,它允许您忽略NULL个值的列。具有两个参数的crosstab()函数可以处理缺少的列。我使用下面的表达式(d.id-1)/13合成了缺失的列 - 这适用于您示例中的数据。

您需要安装additional module tablefunc(每个数据库一次):

CREATE EXTENSION tablefunc;

查找additional explanation and links in this related answer

此功能可以完成以下任务:

CREATE OR REPLACE FUNCTION f_mytbl()
  RETURNS TABLE (id int
, nume text           , prenume text       , cnp numeric
, "nume anterior" text, "stare civila" text, cetatenie text
, rezidenta text      , adresa text        , "tip act" text
, "serie ci" text     , "numar ci" text    , "data eliberarii" text
, "eliberat de" text)
  LANGUAGE plpgsql AS
$BODY$
BEGIN

RETURN QUERY EXECUTE $f$
SELECT *
FROM   crosstab(
    'SELECT (d.id-1)/13 -- AS row_id
          , i.id, d.value
     FROM   infos i
     JOIN   info_data d ON d.id_info = i.id
     ORDER  BY 1, i.id',

    'SELECT id
     FROM   infos
     ORDER  BY id'
    )
AS tbl ($f$ || 'id int,
, nume text           , prenume text       , cnp numeric
, "nume anterior" text, "stare civila" text, cetatenie text
, rezidenta text      , adresa text        , "tip act" text
, "serie ci" text     , "numar ci" text    , "data eliberarii" text
, "eliberat de" text)';

END;
$BODY$;

呼叫:

SELECT * FROM x.mytbl();

不要对嵌套的dollar-quoting感到困惑。

BTW:我用这句话创建了列列表:

SELECT 'id int,' || string_agg(quote_ident(name) || ' ' || type
                              ,', ' ORDER BY i.id) 
FROM   infos i;

答案 1 :(得分:0)

我想你可以做这样的事情

SELECT 
    C1.VALUE AS (SELECT NAME FROM INFOS WHERE ID=1),  
    C2.VALUE AS (SELECT NAME FROM INFOS WHERE ID=2),  
    C3.VALUE AS (SELECT NAME FROM INFOS WHERE ID=3),  
    C4.VALUE AS (SELECT NAME FROM INFOS WHERE ID=4),  
    C5.VALUE AS (SELECT NAME FROM INFOS WHERE ID=5),  
    C6.VALUE AS (SELECT NAME FROM INFOS WHERE ID=6),  
    C7.VALUE AS (SELECT NAME FROM INFOS WHERE ID=7),  
    C8.VALUE AS (SELECT NAME FROM INFOS WHERE ID=8),  
    C9.VALUE AS (SELECT NAME FROM INFOS WHERE ID=9),  
    C10.VALUE AS (SELECT NAME FROM INFOS WHERE ID=10),  
    C11.VALUE AS (SELECT NAME FROM INFOS WHERE ID=11),  
    C12.VALUE AS (SELECT NAME FROM INFOS WHERE ID=12),  
    C13.VALUE AS (SELECT NAME FROM INFOS WHERE ID=13)  
    FROM (
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=1) C1,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=2) C2,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=3) C3,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=4) C4,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=5) C5,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=6) C6,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=7) C7,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=8) C8,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=9) C9,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=10) C10,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=11) C11,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=12) C12,
    (SELECT VALUE FROM INFO_DATA WHERE ID_INFO=13) C13)

仅当您的Infos表中的行数为13时才会这样。

修改

由于列名是动态的,您需要使用存储过程代替直接查询。在这种情况下,您必须使用动态查询。告诉我你是否可以使用存储过程。我可以说出确切的查询。

答案 2 :(得分:0)

如果你不得不对EAV-stuff进行非规范化并且没有类似枢轴的功能,那么这只丑陋的野兽会派上用场。

我必须在info_data中为实体添加一个关键字段。

ALTER TABLE info_data
        ADD column id_key INTEGER
        ;
UPDATE info_data
SET id_key= 1+ ((id-1)/13);

WITH reut AS (
   SELECT d.id_info
        , d.value
        , d.id_key
        , i.zname AS zname
   FROM info_data d
   JOIN infos i ON i.id = d.id_info
        )
, num AS (
        SELECT DISTINCT id_key AS id_key
        FROM info_data
        )
SELECT n.id_key AS id_key
        ,r1.value AS "nume"
        ,r2.value AS "prenume"
        ,r3.value AS "cnp"
        ,r4.value AS "nume anterior"
        ,r5.value AS "stare civila"
        ,r6.value AS "cetatenie"
        ,r7.value AS "rezidenta"
        ,r8.value AS "adresa"
        ,r9.value AS "tip act"
        ,r10.value AS "serie ci"
        ,r11.value AS "numar ci"
        ,r12.value AS "data eliberarii"
        ,r13.value AS "eliberat de"
FROM num n
LEFT JOIN reut r1 ON r1.id_key = n.id_key AND r1.zname = 'nume'
LEFT JOIN reut r2 ON r2.id_key = n.id_key AND r2.zname = 'prenume'
LEFT JOIN reut r3 ON r3.id_key = n.id_key AND r3.zname = 'cnp'
LEFT JOIN reut r4 ON r4.id_key = n.id_key AND r4.zname = 'nume anterior'
LEFT JOIN reut r5 ON r5.id_key = n.id_key AND r5.zname = 'stare civila'
LEFT JOIN reut r6 ON r6.id_key = n.id_key AND r6.zname = 'cetatenie'
LEFT JOIN reut r7 ON r7.id_key = n.id_key AND r7.zname = 'rezidenta'
LEFT JOIN reut r8 ON r8.id_key = n.id_key AND r8.zname = 'adresa'
LEFT JOIN reut r9 ON r9.id_key = n.id_key AND r9.zname = 'tip act'
LEFT JOIN reut r10 ON r10.id_key = n.id_key AND r10.zname = 'serie ci'
LEFT JOIN reut r11 ON r11.id_key = n.id_key AND r11.zname = 'numar ci'
LEFT JOIN reut r12 ON r12.id_key = n.id_key AND r12.zname = 'data eliberarii'
LEFT JOIN reut r13 ON r13.id_key = n.id_key AND r13.zname = 'eliberat de'

ORDER BY n.id_key;

答案 3 :(得分:0)

这就是EAV对大多数事情感到厌烦的原因。如果要在列中检索内容,则可能不希望使用EAV。我提出了自己的EAV-lite,它使用应用程序数据目录和ALTER TABLE命令来创建基于类似于自定义字段的实际关系模型。然而,这不是真的在这里或那里。

基本上你有两种选择。您不能“只使用存储过程”,因为您必须事先告诉PostgreSQL要使用的结果名称和类型。您可以使用返回refcursor和动态SQL的存储过程。这就是满足您严格要求的答案。

这看起来像

CREATE OR REPLACE FUNCTION eav_get(in_id int) RETURNS refcursor
LANGUAGE PLPGSQL AS 
 $$ DECLARE outval refcursor;
            t_row RECORD;
            t_query TEXT;
  BEGIN
      t_query := 'SELECT ';

      FOR t_row IN select distinct "name" FROM infos
      LOOP
         t_query := t_query 'max(CASE WHEN "name" = '|| quote_literal(t_row."name") || ' THEN value ELSE NULL END) ';
      END LOOP;
      t_query := t_query || 'FROM info_data WHERE id_key = || in_id || ' 
                 GROUP BY id_key ';
      OPEN outval FOR EXECUTE t_query;
      RETURN outval;
 END; $$;

我还没有测试过上面的代码,但它应该足够接近你。

但是,您应该考虑另一种选择。这不会在列中返回,但它更清晰,并且在客户端解析时不太难。我们在LedgerSMB中使用它,其中实际需要EAV。

 SELECT id_key, array_agg("name"::text || '=' || "value"::text) 
   FROM infos_data 
  WHERE id_key = ?

假设您不允许使用等号(您可以使用任何其他分隔符),您将获得一个易于为您的应用程序解析的PostgreSQL数组。