إدراج، على التحديث المكرر في postgresql؟
-
12-09-2019 - |
سؤال
قبل عدة أشهر تعلمت من إجابة على تجاوز المكدس كيفية إجراء تحديثات متعددة مرة واحدة في 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)
لدي نفس المشكلة لإدارة إعدادات الحساب كأزواج قيمة الاسم. معايير التصميم هي أن العملاء المختلفين يمكن أن يكون لديهم مجموعات إعدادات مختلفة.
حلاي، على غرار 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.
أفضل الممارسات الحالية التي أدركها هي:
- انسخ البيانات الجديدة / المحدثة في جدول TEMP (بالتأكيد، أو يمكنك إدراجها إذا كانت التكلفة على ما يرام)
- الحصول على قفل [اختياري] (الاستشاري هو الأفضل لأقفال الطاولة، IMO)
- دمج. (الجزء المرح)
سيتم تحديث عدد الصفوف المعدلة. إذا كنت تستخدم 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');