it-swarm.asia

EXISTS (SELECT 1 ...) vs EXISTS (SELECT * ...) واحد أم الآخر؟

كلما احتجت إلى التحقق من وجود صف ما في الجدول ، أميل دائمًا إلى كتابة حالة مثل:

SELECT a, b, c
  FROM a_table
 WHERE EXISTS
       (SELECT *  -- This is what I normally write
          FROM another_table
         WHERE another_table.b = a_table.b
       )

يكتبه بعض الأشخاص الآخرين مثل:

SELECT a, b, c
  FROM a_table
 WHERE EXISTS
       (SELECT 1   --- This Nice '1' is what I have seen other people use
          FROM another_table
         WHERE another_table.b = a_table.b
       )

عندما يكون الشرط NOT EXISTS بدلاً من EXISTS: في بعض المناسبات ، يمكنني كتابته باستخدام LEFT JOIN وحالة إضافية (تسمى أحيانًا antijoin =):

SELECT a, b, c
  FROM a_table
       LEFT JOIN another_table ON another_table.b = a_table.b
 WHERE another_table.primary_key IS NULL

أحاول تجنب ذلك لأنني أعتقد أن المعنى أقل وضوحًا ، خاصة عندما يكون primary_key غير واضح ، أو عندما يكون مفتاحك الأساسي أو شرط الانضمام متعدد الأعمدة (ويمكنك بسهولة نسيان واحد الأعمدة). ومع ذلك ، فأحيانًا تحتفظ برمز مكتوب من قبل شخص آخر ... وهو موجود فقط.

  1. هل هناك فرق (بخلاف النمط) لاستخدام SELECT 1 بدلاً من SELECT *؟
    هل هناك حالة زاوية لا تتصرف فيها بنفس الطريقة؟

  2. على الرغم من أن ما كتبته هو معيار SQL (AFAIK): هل هناك مثل هذا الاختلاف لقواعد البيانات/الإصدارات القديمة المختلفة؟

  3. هل هناك أي ميزة في الكتابة الواضحة على antijoin؟
    هل يعامله المخططون/المحسنون المعاصرون بشكل مختلف عن البند NOT EXISTS؟

42
joanolo

لا ، ليس هناك فرق في الكفاءة بين (NOT) EXISTS (SELECT 1 ...) و (NOT) EXISTS (SELECT * ...) في جميع أنظمة إدارة قواعد البيانات الرئيسية. كثيرا ما رأيت (NOT) EXISTS (SELECT NULL ...) يتم استخدامه أيضًا.

في بعض يمكنك كتابة (NOT) EXISTS (SELECT 1/0 ...) والنتيجة هي نفسها - بدون أي خطأ (قسمة على صفر) ، مما يثبت أن التعبير لم يتم تقييمه حتى.


حول LEFT JOIN / IS NULL طريقة antijoin ، تصحيح: هذا يعادل NOT EXISTS (SELECT ...).

في هذه الحالة ، NOT EXISTS مقابل LEFT JOIN / IS NULL ، قد تحصل على خطط تنفيذ مختلفة. في MySQL على سبيل المثال وفي الإصدارات القديمة (قبل 5.7) ، ستكون الخطط متشابهة إلى حد ما ولكنها ليست متطابقة. إن محسني DBMS الآخرين (SQL Server و Oracle و Postgres و DB2) قادرون - بقدر ما أعرف - على إعادة كتابة هاتين الطريقتين والنظر في نفس الخطط لكليهما. ومع ذلك ، لا يوجد مثل هذا الضمان ، وعند إجراء التحسين ، من الجيد التحقق من الخطط من عمليات إعادة كتابة مكافئة مختلفة حيث قد تكون هناك حالات لا يعيد فيها كل محسن (مثل الاستعلامات المعقدة ، مع العديد من الصلات و/أو الجداول المشتقة/الاستعلامات الفرعية داخل الاستعلام الفرعي ، حيث تتأثر الشروط من جداول متعددة أو أعمدة مركبة مستخدمة في شروط الانضمام) أو خيارات وخطط المحسن بشكل مختلف بالفهارس والإعدادات المتاحة وما إلى ذلك.

لاحظ أيضًا أنه لا يمكن استخدام USING في كل DBMS (SQL Server على سبيل المثال). يعمل JOIN ... ON الأكثر شيوعًا في كل مكان.
ويجب أن تكون الأعمدة مسبوقة باسم الجدول/الاسم المستعار في SELECT لتجنب الأخطاء/الغموض عند انضمامنا.
أنا عادة ما أفضل أيضًا وضع العمود المرتبط في الاختيار IS NULL (على الرغم من أن PK أو أي عمود غير قابل للإلغاء سيكون على ما يرام ، فقد يكون مفيدًا للكفاءة عندما تكون خطة LEFT JOIN فهرسًا غير مجمع):

SELECT a_table.a, a_table.b, a_table.c
  FROM a_table
       LEFT JOIN another_table 
           ON another_table.b = a_table.b
 WHERE another_table.b IS NULL ;

هناك أيضًا طريقة ثالثة للمضادات ، باستخدام NOT IN ولكن هذا له دلالات مختلفة (ونتائج!) إذا كان عمود الجدول الداخلي خاليًا. يمكن استخدامه على الرغم من استبعاد الصفوف التي تحتوي على NULL ، مما يجعل الاستعلام معادلاً للإصدارين السابقين:

SELECT a, b, c
  FROM a_table
 WHERE a_table.b NOT IN 
       (SELECT another_table.b
          FROM another_table
         WHERE another_table.b IS NOT NULL
       ) ;

ينتج عن هذا أيضًا خططًا مماثلة في معظم DBMS.

47
ypercubeᵀᴹ

هناك فئة واحدة من الحالات حيث SELECT 1 و SELECT * غير قابلة للتبديل - وبشكل أكثر تحديدًا ، سيتم قبول إحداهما دائمًا في هذه الحالات ، بينما لا يتم قبول الآخر في الغالب.

أنا أتحدث عن حالات تحتاج فيها إلى التحقق من وجود صفوف لمجموعة مجمعة . إذا كان الجدول T به أعمدة C1 و C2 وتتحقق من وجود مجموعات صف تتطابق مع شرط معين ، يمكنك استخدام SELECT 1 مثله:

EXISTS
(
  SELECT
    1
  FROM
    T
  GROUP BY
    C1
  HAVING
    AGG(C2) = SomeValue
)

ولكن لا يمكنك استخدام SELECT * بنفس الطريقة.

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

ملاحظات إضافية بعد التعليقات

يبدو أن العديد من منتجات قواعد البيانات لا تدعم في الواقع هذا التمييز. ستقبل منتجات مثل SQL Server و Oracle و MySQL و SQLite بكل سرور SELECT * في الاستعلام أعلاه دون أي أخطاء ، مما يعني على الأرجح أنهم يعاملون EXISTS SELECT بطريقة خاصة.

PostgreSQL عبارة عن RDBMS واحد حيث SELECT * قد يفشل ، ولكنه قد يستمر في العمل في بعض الحالات. على وجه الخصوص ، إذا كنت تجمع من قبل PK ، SELECT * ستعمل بشكل جيد ، وإلا ستفشل مع الرسالة:

خطأ: يجب أن يظهر العمود "T.C2" في عبارة GROUP BY أو استخدامه في دالة مجمعة

11
Andriy M

من الطرق المثيرة للجدل لإعادة كتابة عبارة EXISTS التي تؤدي إلى استعلام أنظف ، وربما أقل تضليلًا ، على الأقل في SQL Server ستكون:

SELECT a, b, c
  FROM a_table
 WHERE b = ANY
       (
          SELECT b
          FROM another_table
       );

ستبدو النسخة المضادة لشبه الانضمام مثل:

SELECT a, b, c
  FROM a_table
 WHERE b <> ALL
       (
          SELECT b
          FROM another_table
       );

كلاهما مُحسَّن عادةً على نفس الخطة WHERE EXISTS أو WHERE NOT EXISTS ، لكن النية لا لبس فيها ، وليس لديك "غريب" 1 أو *.

ومن المثير للاهتمام أن مشاكل الاختيار الفارغ المرتبطة بـ NOT IN (...) تسبب مشاكل لـ <> ALL (...) ، في حين أن NOT EXISTS (...) لا تعاني من هذه المشكلة. خذ بعين الاعتبار الجدولين التاليين بعمود فارغ:

IF OBJECT_ID('tempdb..#t') IS NOT NULL
BEGIN
    DROP TABLE #t;
END;
CREATE TABLE #t 
(
    ID INT NOT NULL IDENTITY(1,1)
    , SomeValue INT NULL
);

IF OBJECT_ID('tempdb..#s') IS NOT NULL
BEGIN
    DROP TABLE #s;
END;
CREATE TABLE #s 
(
    ID INT NOT NULL IDENTITY(1,1)
    , SomeValue INT NULL
);

سنضيف بعض البيانات إلى كليهما ، مع بعض الصفوف التي تتطابق ، والبعض الآخر لا:

INSERT INTO #t (SomeValue) VALUES (1);
INSERT INTO #t (SomeValue) VALUES (2);
INSERT INTO #t (SomeValue) VALUES (3);
INSERT INTO #t (SomeValue) VALUES (NULL);

SELECT *
FROM #t;
+ -------- + ----------- + 
 | معرف | SomeValue | 
 + -------- + ----------- + 
 | 1 | 1 | 
 | 2 | 2 | 
 | 3 | 3 | 
 | 4 | NULL | 
 + -------- + ----------- +
INSERT INTO #s (SomeValue) VALUES (1);
INSERT INTO #s (SomeValue) VALUES (2);
INSERT INTO #s (SomeValue) VALUES (NULL);
INSERT INTO #s (SomeValue) VALUES (4);

SELECT *
FROM #s;
+ -------- + ----------- + 
 | معرف | SomeValue | 
 + -------- + ----------- + 
 | 1 | 1 | 
 | 2 | 2 | 
 | 3 | NULL | 
 | 4 | 4 | 
 + -------- + ----------- +

استعلام NOT IN (...):

SELECT *
FROM #t 
WHERE #t.SomeValue NOT IN (
    SELECT #s.SomeValue
    FROM #s 
    );

لديه الخطة التالية:

enter image description here

لا يُرجع الاستعلام أي صفوف لأن قيم NULL تجعل المساواة مستحيلة للتأكيد.

يعرض هذا الاستعلام ، مع <> ALL (...) نفس الخطة ولا يعرض أي صفوف:

SELECT *
FROM #t 
WHERE #t.SomeValue <> ALL (
    SELECT #s.SomeValue
    FROM #s 
    );

enter image description here

يعرض المتغير الذي يستخدم NOT EXISTS (...) شكل خطة مختلفًا قليلاً ويعيد الصفوف:

SELECT *
FROM #t 
WHERE NOT EXISTS (
    SELECT 1
    FROM #s 
    WHERE #s.SomeValue = #t.SomeValue
    );

الخطة:

enter image description here

نتائج هذا الاستعلام:

+ -------- + ----------- + 
 | معرف | SomeValue | 
 + -------- + ----------- + 
 | 3 | 3 | 
 | 4 | NULL | 
 + -------- + ----------- +

هذا يجعل استخدام <> ALL (...) عرضة تمامًا للنتائج الإشكالية مثل NOT IN (...).

5
Max Vernon

والدليل على أنها متطابقة (في MySQL) هو القيام به

EXPLAIN EXTENDED
    SELECT EXISTS ( SELECT * ... ) AS x;
SHOW WARNINGS;

ثم كرر مع SELECT 1. في كلتا الحالتين ، يظهر الناتج "الموسع" أنه تم تحويله إلى SELECT 1.

وبالمثل ، يتم تحويل COUNT(*) إلى COUNT(0).

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

4
Rick James

في بعض قواعد البيانات ، لا يعمل هذا التحسين حتى الآن. على سبيل المثال في PostgreSQL بدءًا من الإصدار 9.6 ، سيفشل ذلك.

SELECT *
FROM ( VALUES (1) ) AS g(x)
WHERE EXISTS (
  SELECT *
  FROM ( VALUES (1),(1) )
    AS t(x)
  WHERE g.x = t.x
  HAVING count(*) > 1
);

وهذا سينجح.

SELECT *
FROM ( VALUES (1) ) AS g(x)
WHERE EXISTS (
  SELECT 1  -- This changed from the first query
  FROM ( VALUES (1),(1) )
    AS t(x)
  WHERE g.x = t.x
  HAVING count(*) > 1
);

إنه فشل بسبب فشل ما يلي ولكن لا يزال هذا يعني وجود فرق.

SELECT *
FROM ( VALUES (1),(1) ) AS t(x)
HAVING count(*) > 1;

يمكنك العثور على مزيد من المعلومات حول هذه المراوغة وانتهاك المواصفات في إجابتي على السؤال هل تتطلب مواصفات SQL مجموعة BY BY في EXISTS ()

4
Evan Carroll