SQL для объединения строк - PullRequest
0 голосов
/ 23 ноября 2011

Обновление

Как выбрать строки из таблицы, в которой при заказе

  • первый элемент соответствует некоторой строке
  • второй элемент соответствует следующей строке
  • третий элемент следующей строки после второй строки
  • четвертый элемент следующей строки после третьей строки
  • и так далее до конца значений в массиве?

Логика

Предположим, у меня есть эти строки в результате запроса (таблица token содержит id и word, а таблица positioning содержит id и position):

 id | word | textblockid |sentence |position 
 5  | Fear |      5      |    1    |    1
 8  | of   |      5      |    1    |    2
 6  | the  |      5      |    1    |    3
 7  | Dark |      5      |    1    |    4
 9  | is   |      5      |    1    |    5

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

Я хочу преобразовать это:

 id  | word             | textblockid | sentence |position 
 10  | Fear of the Dark |      5      |     1    |    1
  9  | is               |      5      |     1    |    2

Я делаю функцию, которая получает массив с идентификаторами для слияния, что-то вроде merge_tokens('{5,8,6,7}').

Я вставляю новое слово Fear of the Dark в таблицу token иполучить сгенерированный идентификатор (как в примере id - это 10).Это просто.

Вопрос

Мне нужно обновить id первого слова (в данном случае Fear) до 10 и удалить следующие слова (of, the, Dark).

Я сомневаюсь, как я могу выполнять эти операции.Я думаю, что мне нужно SELECT из упорядоченной таблицы, где идентификатор первой строки соответствует первому элементу в массиве идентификаторов, идентификатор второй строки соответствует массиву идентификаторов второго элемента и так далее, и после этого обновите первый элемент и удалитеследующие.

Я не могу удалить, просто удалите другие строки по id, потому что они используются в других словах.Я только удалю of, где предыдущий - Fear, следующий - of, а следующий Dark.Следуя этому правилу, я могу удалить только the, где предыдущий - of, другой предыдущий Fear, а следующий - Dark.

. Например, я могу иметь что-то в той же таблицена это нельзя повлиять:

 id  | word      | textblockid |sentence |position 
 6   | the       |      8      |    3    |    10
 11  | sound     |      8      |    3    |    21
 8   | of        |      8      |    3    |    12
 6   | the       |      8      |    3    |    13
 7   | mountain  |      8      |    3    |    14

Ответы [ 5 ]

1 голос
/ 26 ноября 2011

После ответа на большинство ваших недавних вопросов у меня есть смутное представление о том, что вы делаете.Поэтому я внимательно посмотрел на ваше решение и немного оптимизировал его.В основном я упростил код, но есть и существенные улучшения.

Некоторые моменты:

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

На ваше рассмотрение:

CREATE OR REPLACE FUNCTION merge_tokens(words varchar[], separator varchar)
  RETURNS VOID AS
$body$
DECLARE         
    r              record;
    current_id     integer;
    ids            integer[];
    generated_word varchar :=  '';  -- you can initialize variables at declaration time. Saves additional assignment.

BEGIN
    -- get the ids and generate the word
    RAISE NOTICE 'Getting ids and generating words';
    generated_word := array_to_string(words, separator);  -- 1 assignment is much cheaper. Also: no trim() needed.
    ids := ARRAY
    (  SELECT t.id
       FROM  (
          SELECT row_number() OVER () AS rn, text
          FROM  (SELECT unnest(words) AS text) x) y
          JOIN   token t USING (text)
       ORDER  BY rn);
    RAISE NOTICE 'Generated word: %', generated_word;

    -- check if the don't exists to insert it
    SELECT INTO current_id  t.id FROM token t WHERE t.text = generated_word; 
    IF NOT FOUND THEN
        RAISE NOTICE 'Word don''t exists';
        INSERT INTO token(text) VALUES(generated_word)
        RETURNING id
        INTO current_id;  --get the last value without additional query.
    END IF;
    RAISE NOTICE 'Word id: %', current_id;

    -- select the records that will be updated
    RAISE NOTICE 'Getting words to be updated.';
    FOR r IN
        SELECT textblockid, sentence, position, tokenid, rn
        FROM
        ( -- select the rows that are complete
          SELECT textblockid, sentence, position, tokenid, rn, count(*) OVER (PARTITION BY grp) AS counting
          FROM
          ( -- match source with lookup table
                SELECT source.textblockid, source.sentence, source.position, source.tokenid, source.rn, source.grp
                FROM
                (   -- select textblocks where words appears with row number to matching
                     SELECT tb.textblockid, tb.sentence, tb.position, tb.tokenid, grp
                                           ,CASE WHEN grp > 0 THEN
                                            row_number() OVER (PARTITION BY grp ORDER BY tb.textblockid, tb.sentence, tb.position)
                                            END AS rn               
                     FROM
                     (   -- create the groups to be used in partition by to generate the row numbers
                          SELECT tb.textblockid, tb.sentence, tb.position, tb.tokenid
                                ,SUM(CASE WHEN tb.tokenid = ids[1] THEN 1 ELSE 0 END) OVER (ORDER BY tb.textblockid, tb.sentence, tb.position) AS grp
                          FROM  textblockhastoken tb
                          JOIN
                          (   --select the textblocks where the word appears
                                SELECT textblockid, sentence
                                FROM   textblockhastoken tb
                                WHERE  tb.tokenid = ids[1]
                          ) res USING (textblockid, sentence)
                     ) tb
                ) source
                -- create the lookup table to match positions
                JOIN (SELECT row_number() OVER () as rn, id FROM unnest(ids) AS id) lookup USING (rn)
                WHERE source.tokenid = lookup.id
          ) merged
        ) g  
        WHERE g.counting = array_length(ids,1)
        ORDER BY g.rn --order by row number to update first, delete and change positions after
    LOOP
        --check if update or delete
        IF (r.rn = 1) THEN
            RAISE NOTICE 'Updating word in T:% S:% P:%', r.textblockid, r.sentence, r.position;
            UPDATE textblockhastoken tb SET tokenid = current_id
            WHERE (tb.textblockid, tb.sentence, tb.position)
                = ( r.textblockid,  r.sentence,  r.position);
        ELSE
            RAISE NOTICE 'Deleting word in T:% S:% P:%', r.textblockid, r.sentence, r.position;
            DELETE FROM textblockhastoken tb
            WHERE (tb.textblockid, tb.sentence, tb.position)
                = ( r.textblockid,  r.sentence,  r.position);
        END IF;
        --check if is the last word to update the positions
        IF (r.rn = array_length(ids,1)) THEN
            RAISE NOTICE 'Changing positions in T:% S:%', r.textblockid, r.sentence;
            UPDATE textblockhastoken tb SET position = new_position
            FROM
            (   SELECT textblockid, sentence, position
                      ,row_number() OVER (PARTITION BY tb.textblockid, tb.sentence ORDER BY tb.position) as new_position
                FROM   textblockhastoken tb
                WHERE  tb.textblockid = r.textblockid AND tb.sentence = r.sentence
            ) np
            WHERE (tb.textblockid, tb.sentence, tb.position)
                = (np.textblockid, np.sentence, np.position)
            AND    tb.position <> np.new_position;
        END IF;
    END LOOP;
END;
$body$ LANGUAGE plpgsql;
1 голос
/ 23 ноября 2011

Лучше всего сделать это за одну транзакцию:

UPDATE token
SET    word = (
    SELECT string_agg(word, ' '  ORDER BY position)
    FROM   token
    WHERE  id = ANY('{5,8,6,7}'::int[])
    )
      ,id = nextval('token_id_seq')
WHERE  id = ('{5,8,6,7}'::int[])[1];

DELETE FROM token
WHERE  id = ANY('{5,8,6,7}'::int[])
AND    id <> ('{5,8,6,7}'::int[])[1];

Замените '{5,8,6,7}'::int[] параметром массива целых чисел.
Я получаю новый id из последовательности, которая, я полагаю, существует.
Далее я предполагаю, что упорядочение в массиве совпадает с упорядочением по позиции. Альтернативная версия приведена ниже.
id для обновления является первым элементом массива.

Упорядочение слов может быть выполнено внутри агрегатной функции (начиная с PostgreSQL 9.0). Прочитайте об этом в руководстве .


Ответ на дополнительный вопрос

Упорядочить выбранные строки в соответствии с последовательностью элементов массива:

SELECT rn, t.*
FROM   (
    SELECT id
          ,row_number() OVER () AS rn
    FROM (SELECT unnest('{5,8,6,7}'::int[]) id) x
    )  x
JOIN   token t USING (id)
ORDER  BY rn;

Или ... делает то же самое с разными техниками, работает и в старых версиях Postgres:

SELECT rn, t.*
FROM   (
    SELECT rn
          ,a[rn] AS id
    FROM (SELECT '{5,8,6,7}'::int[] AS a
                ,generate_series(1, array_upper('{5,8,6,7}'::int[], 1)) rn) x
    )  x
JOIN   token t USING (id)
ORDER  BY rn;

Комбинация

Используйте это в выражении UPDATE:

UPDATE token
SET    word = (
    SELECT string_agg(word, ' '  ORDER BY rn)
    FROM   (
    SELECT rn
          ,a[rn] AS id
    FROM  (
           SELECT '{5,8,6,7}'::int[] AS a
                 ,generate_series(1, array_upper('{5,8,6,7}'::int[], 1)) rn) x
          ) x
    JOIN   token t USING (id)
    )
      ,id = nextval('token_id_seq')
WHERE  id = ('{5,8,6,7}'::int[])[1];
1 голос
/ 24 ноября 2011

Этот фрагмент не использует массивы. (Я не люблю массивы)

set search_path='tmp';

DROP TABLE wordlist;
CREATE TABLE wordlist
    ( id INTEGER NOT NULL PRIMARY KEY
    , word varchar
    , textblockid INTEGER NOT NULL
    , sentence INTEGER NOT NULL
    , postion INTEGER NOT NULL
    , UNIQUE (textblockid,sentence,postion)
    );

INSERT INTO wordlist(id,word,textblockid,sentence,postion) VALUES
 (5 , 'Fear', 5 , 1 , 1 )
,(8 , 'of', 5 , 1 , 2 )
,(6 , 'the', 5 , 1 , 3 )
,(7 , 'Dark', 5 , 1 , 4 )
,(9 , 'is', 5 , 1 , 5 )
    ;

WITH RECURSIVE meuk AS (
    SELECT 0 AS lev
        , id,word AS words
        , textblockid,sentence,postion AS lastpos
    FROM wordlist
    UNION
    SELECT 1+ mk.lev AS lev
        , wl.id
        , mk.words || ' '::text || wl.word AS words
        , wl.textblockid,wl.sentence
        , wl.postion AS lastpos
    FROM meuk mk
    JOIN wordlist wl ON (wl.textblockid = mk.textblockid
        AND wl.sentence = mk.sentence
        AND wl.postion = mk.lastpos+1)
    )
SELECT * FROM meuk
WHERE lev = 3
    ;

Результаты:

SET
DROP TABLE
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "wordlist_pkey" for table "wordlist"
NOTICE:  CREATE TABLE / UNIQUE will create implicit index "wordlist_textblockid_sentence_postion_key" for table "wordlist"
CREATE TABLE
INSERT 0 5
 lev | id |      words       | textblockid | sentence | lastpos 
-----+----+------------------+-------------+----------+---------
   3 |  7 | Fear of the Dark |           5 |        1 |       4
   3 |  9 | of the Dark is   |           5 |        1 |       5
(2 rows)
0 голосов
/ 24 ноября 2011

Этот ответ для моего конкретного случая. Я не знаю, является ли это лучшим способом, но работает на меня.

Я строю эту процедуру с ответом на следующие вопросы: Возможно ли иметь разные условия для каждой строки в запросе? и Как создать ОКНО в PostgreSQL, пока одно и то же значение не появится снова?

FOREARCH работает только в PostgreSQL 9.1.

CREATE OR REPLACE FUNCTION merge_tokens(words VARCHAR[], separator VARCHAR)
RETURNS VOID
AS $$
DECLARE         
    r RECORD;
    current_id INTEGER;
    current_word VARCHAR;       
    ids INTEGER[];
    generated_word VARCHAR;

BEGIN       
    -- get the ids and generate the word
    RAISE NOTICE 'Getting ids and generating words';
    generated_word = '';
    FOREACH current_word IN ARRAY words
    LOOP BEGIN                      
        generated_word = generated_word || current_word;
        generated_word = generated_word || separator;
        SELECT t.id INTO current_id FROM token t WHERE t.text = current_word;
        ids = ids || current_id;
    END;
    END LOOP;

    -- remove lead and ending spacing in word
    RAISE NOTICE 'Generated word: %', generated_word;
    generated_word = TRIM(generated_word);

    -- check if the don't exists to insert it
    SELECT t.id INTO current_id FROM token t WHERE t.text = generated_word; 
    IF (current_id IS NULL) THEN
        RAISE NOTICE 'Word don''t exists';
        INSERT INTO token(id,text) VALUES(nextval('tokenidsqc'),generated_word);
        current_id = lastval(); --get the last value from the sequence      
    END IF;
    RAISE NOTICE 'Word id: %', current_id;

    -- select the records that will be updated
    RAISE NOTICE 'Getting words to be updated.';
    FOR r IN SELECT grouping.textblockid, grouping.sentence, grouping.position, grouping.tokenid, grouping.row_number
    FROM
    (
        -- select the rows that are complete
        SELECT merged.textblockid, merged.sentence, merged.position, merged.tokenid,merged.row_number,count(*) OVER w as counting           
        FROM
        (
            -- match source with lookup table
            SELECT source.textblockid, source.sentence, source.position, source.tokenid,source.row_number, source.grp
            FROM
            (   -- select textblocks where words appears with row number to matching
                SELECT tb.textblockid, tb.sentence, tb.position, tb.tokenid, grp,
                    CASE WHEN grp > 0 THEN
                        row_number() OVER (PARTITION BY grp ORDER BY tb.textblockid,tb.sentence,tb.position)
                    END AS row_number               
                FROM
                (   -- create the groups to be used in partition by to generate the row numbers
                    SELECT tb.textblockid, tb.sentence, tb.position, tb.tokenid,
                        SUM(CASE WHEN tb.tokenid = ids[1] THEN 1 ELSE 0 END) OVER (ORDER BY tb.textblockid,tb.sentence,tb.position) AS grp
                    FROM textblockhastoken tb,
                    (   --select the textblocks where the word appears
                        SELECT textblockid, sentence
                        FROM textblockhastoken tb
                        WHERE tb.tokenid = ids[1]
                    )res
                    WHERE tb.textblockid = res.textblockid
                    AND tb.sentence = res.sentence                      
                )tb
            )source,
            -- create the lookup table to match positions
            (
                SELECT row_number() OVER () as row_number,id FROM unnest(ids::INTEGER[]) as id
            )lookup
            WHERE source.tokenid = lookup.id
            AND source.row_number = lookup.row_number
        )merged
        WINDOW w AS (PARTITION BY grp)
    ) grouping      
    WHERE grouping.counting = array_length(ids,1)
    ORDER BY grouping.row_number --order by row number to update first, delete and change positions after
    -- end of query and start of iterations actions
    LOOP BEGIN
        --check if update or delete
        IF (r.row_number = 1) THEN
            RAISE NOTICE 'Updating word in T:% S:% P:%', r.textblockid, r.sentence, r.position;
            UPDATE textblockhastoken tb SET tokenid = current_id
            WHERE tb.textblockid = r.textblockid 
            AND tb.sentence = r.sentence
            AND tb.position = r.position;
        ELSE
            RAISE NOTICE 'Deleting word in T:% S:% P:%', r.textblockid, r.sentence, r.position;
            DELETE FROM textblockhastoken tb
            WHERE tb.textblockid = r.textblockid 
            AND tb.sentence = r.sentence
            AND tb.position = r.position;
        END IF;
        --check if is the last word to update the positions
        IF (r.row_number = array_length(ids,1)) THEN
            RAISE NOTICE 'Changing positions in T:% S:%', r.textblockid, r.sentence;
            UPDATE textblockhastoken tb SET position = new_position
            FROM
            (   
                SELECT textblockid, sentence, position, row_number() OVER w as new_position
                FROM textblockhastoken tb
                WHERE tb.textblockid = r.textblockid AND tb.sentence = r.sentence
                WINDOW w AS (PARTITION BY tb.textblockid, tb.sentence ORDER BY tb.position)             
            )new_positioning                
            WHERE tb.textblockid = new_positioning.textblockid 
            AND tb.sentence = new_positioning.sentence
            AND tb.position = new_positioning.position
            AND tb.position <> new_positioning.new_position;
        END IF;
    END;
    END LOOP;
END 
$$
LANGUAGE plpgsql;
0 голосов
/ 23 ноября 2011

Это то, что вы могли бы сделать как часть вашей функции merge_tokens?Похоже, вы могли просто заставить эту функцию отслеживать, какие записи нужно обновить / удалить, просто на основе предоставленного массива (первый элемент обновлен, остальные удалены).

...