سؤال

قبل عدة أشهر تعلمت من إجابة على تجاوز المكدس كيفية إجراء تحديثات متعددة مرة واحدة في MySQL باستخدام بناء الجملة التالي:

INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);

لقد تحولت الآن إلى postgresql ويبدو أن هذا غير صحيح. إنها تشير إلى جميع الجداول الصحيحة لذلك أفترض أنها مسألة تستخدم كلمات رئيسية مختلفة، لكنني لست متأكدا من أين يتم تغطية هذا في وثائق postgresql.

لتوضيح، أريد إدراج العديد من الأشياء وإذا كانت موجودة بالفعل لتحديثها.

هل كانت مفيدة؟

المحلول

postgresql منذ الإصدار 9.5 upsert. بناء جملة، مع على الصراع بند. مع بناء الجملة التالي (على غرار MySQL)

INSERT INTO the_table (id, column_1, column_2) 
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE 
  SET column_1 = excluded.column_1, 
      column_2 = excluded.column_2;

البحث في Archives PostgresQL في أرشيف مجموعة "Upsert" يؤدي إلى العثور على مثال على القيام بما تريد القيام به، في الدليل:

مثال 38-2. استثناءات مع التحديث / إدراج

يستخدم هذا المثال معالجة الاستثناء لأداء أي تحديث أو إدراج، حسب الاقتضاء:

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        -- note that "a" must be unique
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- do nothing, and loop to try the UPDATE again
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

ربما يكون هناك مثال على كيفية القيام بذلك بكميات كبيرة، باستخدام CTES في 9.1 وما فوق، في القراصنة القائمة البريدية:

WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;

يرى إجابة A_horse_with_no_name. للحصول على مثال أوضح.

نصائح أخرى

تحذير: هذا غير آمن إذا أعدم من جلسات متعددة في نفس الوقت (انظر تحذيرات أدناه).


طريقة أخرى ذكية للقيام "Upsert" في postgresql هي القيام ببيانين تحديث / إدراج متتابعين يتم تصميم كل منها لتحقيق النجاح أو عدم وجود تأثير.

UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
       SELECT 3, 'C', 'Z'
       WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);

سينجح التحديث في حالة وجود صف مع "معرف = 3" بالفعل، وإلا فلن يكون له أي تأثير.

سينجح الإدراج فقط إذا كان الصف مع "معرف = 3" غير موجود بالفعل.

يمكنك الجمع بين هذين في سلسلة واحدة وتشغيلها كلاهما مع عبارة SQL واحدة تنفيذه من التطبيق الخاص بك. يوصى بشدة بتشغيلها معا في معاملة واحدة.

يعمل هذا بشكل جيد للغاية عند التشغيل بمعزل أو على جدول مغلق، ولكنه يخضع لظروف السباق التي تعني أنها قد لا تزال لا تزال تفشل مع خطأ مفتاح مكرر إذا تم إدراج صف بشكل متزامن، أو قد ينتهي بدون صف يتم إدراجه عند حذف صف متزامن وبعد أ SERIALIZABLE سيتم التعامل مع المعاملة في PostgresQL 9.1 أو أعلى بشكل موثوق بتكلفة معدل فشل التسلسل العالي للغاية، مما يعني أنك ستحتاج إلى إعادة المحاولة كثيرا. يرى لماذا يتعارض معقدة جدا, ، الذي يناقش هذه الحالة بمزيد من التفاصيل.

هذا النهج هو أيضا يخضع لتفقد التحديثات في read committed العزلة ما لم يكن التطبيق يتحقق من التهم الصف المتأثر والتحقق من ذلك إما insert أو ال update تتأثر الصف.

مع postgresql 9.1 يمكن تحقيق ذلك باستخدام CTE غير قابل للكتابة (تعبير الجدول المشترك):

WITH new_values (id, field1, field2) as (
  values 
     (1, 'A', 'X'),
     (2, 'B', 'Y'),
     (3, 'C', 'Z')

),
upsert as
( 
    update mytable m 
        set field1 = nv.field1,
            field2 = nv.field2
    FROM new_values nv
    WHERE m.id = nv.id
    RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1 
                  FROM upsert up 
                  WHERE up.id = new_values.id)

انظر إدخالات بلوق هذه:


لاحظ أن هذا الحل يفعل ليس منع انتهاك رئيسي فريد ولكن ليس عرضة للتحديثات المفقودة.
انظر متابعة بواسطة Craig Ringer على dba.stackexchange.com

في postgresql 9.5 والأحدث يمكنك استخدامها INSERT ... ON CONFLICT UPDATE.

يرى وثائق.

mysql. INSERT ... ON DUPLICATE KEY UPDATE يمكن إعادة صياغة مباشرة إلى ON CONFLICT UPDATE. وبعد لا يوجد بناء جملة SQL-Standard، كلاهما ملحقات خاصة بقاعدة البيانات. هناك أسباب وجيهة MERGE لم يكن يستخدم لهذا, ، لم يتم إنشاء بناء جملة جديد للمتعة فقط. (يحتوي بناء جملة MySQL أيضا على مشكلات تعني أنه لم يتم اعتماده مباشرة).

على سبيل المثال الإعداد المعطى:

CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);

استعلام MySQL:

INSERT INTO tablename (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

يصبح:

INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;

اختلافات:

  • أنت يجب حدد اسم العمود (أو اسم قيد فريد) لاستخدامه لفحص التفرد. هذا هو ON CONFLICT (columnname) DO

  • الكلمة الرئيسية SET يجب أن تستخدم، كما لو كان هذا طبيعي UPDATE بيان

لديها بعض الميزات لطيفة جدا:

  • يمكنك الحصول على WHERE جملة على حسابك UPDATE (تركك بدوره بشكل فعال ON CONFLICT UPDATE إلى ON CONFLICT IGNORE لقيم معينة)

  • تتوفر قيم الإدراج المقترحة مثل متغير الصف EXCLUDED, ، والتي لها نفس هيكل الجدول المستهدف. يمكنك الحصول على القيم الأصلية في الجدول باستخدام اسم الجدول. لذلك في هذه الحالة EXCLUDED.c سوف يكون 10 (لأن هذا ما حاولنا إدراجه) و "table".c سوف يكون 3 لأن هذه هي القيمة الحالية في الجدول. يمكنك استخدام إما أو كليهما في SET التعبيرات و WHERE بند.

للخلفية على Upsert انظر كيفية التكفير (دمج، أدخل ... على التحديث المكرر) في postgresql؟

كنت أبحث عن نفس الشيء عندما جئت إلى هنا، لكن الافتقار إلى وظيفة "Upsert" العامة تزعجني قليلا حتى اعتقدت أنك يمكن أن تمر بالتحديث وإدراج SQL كحجج في هذه الوظيفة تشكل الدليل

هذا من شأنه أن يبدو مثل هذا:

CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
    RETURNS VOID
    LANGUAGE plpgsql
AS $$
BEGIN
    LOOP
        -- first try to update
        EXECUTE sql_update;
        -- check if the row is found
        IF FOUND THEN
            RETURN;
        END IF;
        -- not found so insert the row
        BEGIN
            EXECUTE sql_insert;
            RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- do nothing and loop
        END;
    END LOOP;
END;
$$;

وربما أن تفعل ما أرادته في البداية القيام به، والدفعة "Upsert"، فيمكنك استخدام TCL لتقسيم SQL_UPDATE وحلقة التحديثات الفردية، وستكون Hitformance Hit صغيرة جدا http://archives.postgresql.org/pgsql-performance/2006-04/msg00557.php.

أعلى تكلفة تنفذ الاستعلام من التعليمات البرمجية الخاصة بك، في جانب قاعدة البيانات تكاليف التنفيذ أصغر بكثير

لا يوجد أمر بسيط للقيام بذلك.

النهج الأكثر صحة هو استخدام الوظيفة، مثل واحد من مستندات.

حل آخر (على الرغم من أن ليس بالأمان) هو القيام بتحديث مع العودة، والتحقق من الصفوف التي تم تحديثها، وإدخال البقية منهم

شيء على غرار:

update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;

بافتراض معرف: تم إرجاع 2:

insert into table (id, column) values (1, 'aa'), (3, 'cc');

بالطبع سيتم إنقاذها عاجلا أم آجلا (في بيئة متزامنة)، حيث توجد حالة سباق واضحة هنا، ولكن عادة ما ستعمل.

هنا مقال أطول وأكثر شمولا حول هذا الموضوع.

شخصيا، قمت بإعداد "قاعدة" مرتبطة ببيان الإدراج. قل أنه كان لديك جدول "DNS" الذي سجل يضرب DNS لكل عميل على أساس سنوي:

CREATE TABLE dns (
    "time" timestamp without time zone NOT NULL,
    customer_id integer NOT NULL,
    hits integer
);

كنت تريد أن تكون قادرا على إعادة إدخال الصفوف ذات القيم المحدثة، أو إنشاءها إذا لم تكن موجودة بالفعل. مفصل على customer_id والوقت. شيء من هذا القبيل:

CREATE RULE replace_dns AS 
    ON INSERT TO dns 
    WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time") 
            AND (dns.customer_id = new.customer_id)))) 
    DO INSTEAD UPDATE dns 
        SET hits = new.hits 
        WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));

تحديث: هذا لديه القدرة على الفشل إذا حدث إدراجات متزامنة، لأنها ستؤدي إلى استثناءات فريدة من نوعها. ومع ذلك، ستستمر المعاملة غير المؤهلة وننجح، وتحتاج فقط إلى تكرار المعاملة المنتهية.

ومع ذلك، إذا حدثت أطنان من إدراجها طوال الوقت، فسترغب في وضع قفل الجدول حول عبارات "إدراج": ستشارك "مشاركة صف حصرية" سيمنع أي عمليات يمكن إدراجها أو حذفها أو تحديث الصفوف في جدولك المستهدف. ومع ذلك، فإن التحديثات التي لا تقوم بتحديث المفتاح الفريد آمن، لذلك إذا لم تفعل أي عملية، فستفعل ذلك، استخدم الأقفال الاستشارية بدلا من ذلك.

أيضا، لا يستخدم الأمر النسخة قواعد، لذلك إذا كنت تدرج مع نسخ، فستحتاج إلى استخدام المشغلات بدلا من ذلك.

أنا مخصص "Upsert" وظيفة أعلاه، إذا كنت ترغب في إدراج واستبدال:

`

 CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)

 RETURNS void AS
 $BODY$
 BEGIN
    -- first try to insert and after to update. Note : insert has pk and update not...

    EXECUTE sql_insert;
    RETURN;
    EXCEPTION WHEN unique_violation THEN
    EXECUTE sql_update; 
    IF FOUND THEN 
        RETURN; 
    END IF;
 END;
 $BODY$
 LANGUAGE plpgsql VOLATILE
 COST 100;
 ALTER FUNCTION upsert(text, text)
 OWNER TO postgres;`

وبعد التنفيذ، افعل شيئا مثل هذا:

SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)

من المهم وضع Double Dollar-Comma لتجنب أخطاء المحول البرمجي

  • تحقق من السرعة ...

مماثلة للإجابة الأكثر محبوبة، ولكنها تعمل بشكل أسرع قليلا:

WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)

(مصدر: http://www.the-art-art-of-web.com/sql/upsert/)

لدي نفس المشكلة لإدارة إعدادات الحساب كأزواج قيمة الاسم. معايير التصميم هي أن العملاء المختلفين يمكن أن يكون لديهم مجموعات إعدادات مختلفة.

حلاي، على غرار JWP هو محو السائبة واستبدالها، وإنشاء سجل الدمج داخل التطبيق الخاص بك.

هذه مقاومة للرصاص، منصة مستقلة، وبما أن هناك أكثر من حوالي 20 إعدادات لكل عميل، فهذه ليست سوى 3 مكالمات منخفضة إلى حد ما DB DB - ربما أسرع طريقة.

بديل تحديث الصفوف الفردية - التحقق من الاستثناءات ثم إدراج - أو مزيج من التعليمات البرمجية البشعة، بطيئة وكسر في كثير من الأحيان لأنه (كما هو مذكور أعلاه) يتغير معالجة استثناء SQL غير القياسية من DB إلى DB - أو حتى حرر للإصدار.

 #This is pseudo-code - within the application:
 BEGIN TRANSACTION - get transaction lock
 SELECT all current name value pairs where id = $id into a hash record
 create a merge record from the current and update record
  (set intersection where shared keys in new win, and empty values in new are deleted).
 DELETE all name value pairs where id = $id
 COPY/INSERT merged records 
 END TRANSACTION

وفقا ل توثيق postgresql لل INSERT بيان, ، التعامل مع ON DUPLICATE KEY القضية غير مدعومة. هذا الجزء من بناء الجملة هو ملحق MySQL الخاص.

CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
  RETURNS boolean AS
$BODY$
BEGIN
    UPDATE users SET name = _name WHERE id = _id;
    IF FOUND THEN
        RETURN true;
    END IF;
    BEGIN
        INSERT INTO users (id, name) VALUES (_id, _name);
    EXCEPTION WHEN OTHERS THEN
            UPDATE users SET name = _name WHERE id = _id;
        END;
    RETURN TRUE;
END;

$BODY$
  LANGUAGE plpgsql VOLATILE STRICT

أنا استخدم دمج هذه الوظيفة

CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
  RETURNS void AS
$BODY$
BEGIN
    IF EXISTS(SELECT a FROM tabla WHERE a = key)
        THEN
            UPDATE tabla SET b = data WHERE a = key;
        RETURN;
    ELSE
        INSERT INTO tabla(a,b) VALUES (key, data);
        RETURN;
    END IF;
END;
$BODY$
LANGUAGE plpgsql

لدمج مجموعات صغيرة، باستخدام الوظيفة أعلاه على ما يرام. ومع ذلك، إذا كنت دمج كميات كبيرة من البيانات، أقترح النظر http://mbk.projects.postgresql.org.

أفضل الممارسات الحالية التي أدركها هي:

  1. انسخ البيانات الجديدة / المحدثة في جدول TEMP (بالتأكيد، أو يمكنك إدراجها إذا كانت التكلفة على ما يرام)
  2. الحصول على قفل [اختياري] (الاستشاري هو الأفضل لأقفال الطاولة، IMO)
  3. دمج. (الجزء المرح)

سيتم تحديث عدد الصفوف المعدلة. إذا كنت تستخدم JDBC (Java)، يمكنك بعد ذلك التحقق من هذه القيمة مقابل 0، وإذا لم تتأثر أي صفوف، فأرسلها بدلا من ذلك. إذا كنت تستخدم بعض لغة البرمجة الأخرى، فربما لا يزال عدد الصفوف المعدلة يمكن الحصول عليها، تحقق من الوثائق.

قد لا يكون هذا أنيقا ولكن لديك SQL SQL أكثر بساطة أكثر تافهة للاستخدام من رمز الاتصال. بشكل مختلف، إذا قمت بكت بكتابة البرنامج النصي ذو عشر خطا في PL / PSQL، فربما يجب أن يكون لديك اختبار وحدة من نوع أو آخر فقط لذلك وحده.

يحرر: هذا لا يعمل كما هو متوقع. على عكس الإجابة المقبولة، ينتج هذا انتهاكات أساسية فريدة من نوعها عند الاتصال بمرارا وتكرارا upsert_foo في وقت واحد.

Eureka! لقد اكتشفت طريقة للقيام بذلك في استعلام واحد: استخدام UPDATE ... RETURNING لاختبار ما إذا كانت أي صفوف قد تأثرت:

CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);

CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
    UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;

CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
    INSERT INTO foo
        SELECT $1, $2
        WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;

ال UPDATE يجب أن يتم ذلك في إجراء منفصل لأنه لسوء الحظ، هذا خطأ في بناء الجملة:

... WHERE NOT EXISTS (UPDATE ...)

الآن يعمل حسب الرغبة:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top