1

Is there a better way to write the following in my where clause?

WHERE (IIf([GrpOrder]=3,IIf([LabelText]="Totals",True,False),True)) =True))

Thanks,

Scott

Scott Silvi
  • 3,059
  • 5
  • 40
  • 63

3 Answers3

3

I assume your code contains typos (unblanaced parentheses) and should in fact read:

WHERE IIf([GrpOrder]=3,IIf([LabelText]="Totals",True,False),True) = true

From a SQL code perspective there are actually nine cases to consider because of SQL's three value logic with NULL:

GrpOrder = 3
GrpOrder <> 3
GrpOrder IS NULL

LabelText = 'Totals'
LabelText <> 'Totals'
LabelText IS NULL

In combination there are nine cases e.g. test data and results:

OP_result | GrpOrder | LabelText  
----------------------------------
     TRUE |       55 | 'Totals'
     TRUE |       55 | 'Tallies'
     TRUE |       55 | <NULL>
     TRUE |        3 | 'Totals'
    FALSE |        3 | 'Tallies'
    FALSE |        3 | <NULL>
     TRUE |   <NULL> | 'Totals'
     TRUE |   <NULL> | 'Tallies'
     TRUE |   <NULL> | <NULL>

The safest approach would be to write out a series of OR clauses, explcitly handling NULL for both column for each OR clause. However, that is very long winded it would be better to taget those two cases that return FALSE. And this is where most folk (including me!) run into problems with NULL: it's just too counter-intuitive!

For example, it is tempting to write this:

(GrpOrder = 3 AND LabelText IS NULL)
OR
(GrpOrder = 3 AND LabelText <> 'Totals')

then 'flip' its value using NOT:

NOT (
     (GrpOrder = 3 AND LabelText IS NULL)
     OR
     (GrpOrder = 3 AND LabelText <> 'Totals')
    )

However, in doing so NULL sneaks into the resultset:

OP_result | attempt_1 | GrpOrder | LabelText  
---------------------------------------------
     TRUE |      TRUE |       55 | 'Totals'
     TRUE |      TRUE |       55 | 'Tallies'
     TRUE |      TRUE |       55 | <NULL>
     TRUE |      TRUE |        3 | 'Totals'
    FALSE |     FALSE |        3 | 'Tallies'
    FALSE |     FALSE |        3 | <NULL>
     TRUE |      TRUE |   <NULL> | 'Totals'
     TRUE |    <NULL> |   <NULL> | 'Tallies'
     TRUE |    <NULL> |   <NULL> | <NULL>

So we need to explicitly handle more cases than it might appear at first glance.

The simplest predicate I could come up with that gives the desired result in Access:

NOT
(
 (LabelText <> 'Totals' OR LabelText IS NULL)
 AND GrpOrder = 3 
 AND GrpOrder IS NOT NULL
)

[...which is so odd to read I wonder whether the OP's code is yielding the desired result in the first place.]

The main lessons to learn:

  • NULL in SQL should be avoided: it is counter-intuitive even causes bugs even by very experienced SQL coders.
  • Always post your schema (e.g. CREATE TABLE SQL DDL...) and sample data (... e.g. INSERT INTO SQL DML...) with expected results (... or use words and pictures if you must ;) because if your columns are marked as NOT NULL then the answer is very much simpler! :)

@Yanir Kleiman comments:

GrpOrder can't be 3 and NULL at the same time, so checking it is not null in this case is redundant

One could be forgiven for thinking so. But this is Access :) We have excellent specs for SQL products that claim compliance with the SQL Standards. Access claims no such compliance and the documentation the Access Team have provided is of a particularly low quality.

Rather, in Access-land, for something to be true, you have to actually test it!

When I remove the predicate

AND GrpOrder IS NOT NULL

nulls appear in the resultset. While it feels like this 'defies logic', bear in mind that SQL's three value logic is only defined in a spec to which Access claims no compliance. If the Access Team don't tell us how the product is supposed to work, how can we tell whether the above is a bug or a feature? And even if we could convince them it is a bug, would they fix it?

Below I provide VBA code to reproduce the issue: just copy+paste into any VBA module, no references need to be set. It creates a new .mdb in the temp folder, then creates the table and test data. Access need not be installed on the machine e.g. use Excel's VBA editor.

The messagebox shows shows the resultset when the above predicate is included and removed respectively. In addition to the two table columns, two calculated columns show with values -1 (TRUE), 0 (FALSE) and NULL and the leftmost one is the OP's:

Sub AccessStrangeLogic()

  On Error Resume Next
  Kill Environ$("temp") & "\DropMe.mdb"
  On Error GoTo 0

  Dim cat
  Set cat = CreateObject("ADOX.Catalog")
  With cat
    .Create _
        "Provider=Microsoft.Jet.OLEDB.4.0;" & _
        "Data Source=" & _
        Environ$("temp") & "\DropMe.mdb"
    With .ActiveConnection

      Dim Sql As String
      Sql = _
      "CREATE TABLE GrpOrders" & vbCr & _
      "(" & vbCr & _
      " GrpOrder INTEGER," & vbCr & _
      " LabelText NVARCHAR(10)" & vbCr & _
      ");"
      .Execute Sql

      Sql = _
      "INSERT INTO GrpOrders (GrpOrder, LabelText)" & _
      " VALUES (55, 'Totals');"
      .Execute Sql
      Sql = _
      "INSERT INTO GrpOrders (GrpOrder, LabelText)" & _
      " VALUES (55, 'Tallies');"
      .Execute Sql
      Sql = _
      "INSERT INTO GrpOrders (GrpOrder, LabelText)" & _
      " VALUES (55, NULL);"
      .Execute Sql
      Sql = _
      "INSERT INTO GrpOrders (GrpOrder, LabelText)" & _
      " VALUES (3, 'Totals');"
      .Execute Sql
      Sql = _
      "INSERT INTO GrpOrders (GrpOrder, LabelText)" & _
      " VALUES (3, 'Tallies');"
      .Execute Sql
      Sql = _
      "INSERT INTO GrpOrders (GrpOrder, LabelText)" & _
      " VALUES (3, NULL);"
      .Execute Sql
      Sql = _
      "INSERT INTO GrpOrders (GrpOrder, LabelText)" & _
      " VALUES (NULL, 'Totals');"
      .Execute Sql
      Sql = _
      "INSERT INTO GrpOrders (GrpOrder, LabelText)" & _
      " VALUES (NULL, 'Tallies');"
      .Execute Sql
      Sql = _
      "INSERT INTO GrpOrders (GrpOrder, LabelText)" & _
      " VALUES (NULL, NULL);"
      .Execute Sql

      ' Include "AND GrpOrder IS NOT NULL"
      Sql = _
      "SELECT *, " & vbCr & _
      "       IIf([GrpOrder]=3,IIf([LabelText]=""Totals"",True,False),True) = true AS OP_result, " & vbCr & _
      "       NOT" & vbCr & _
      "       (" & vbCr & _
      "        (LabelText <> 'Totals' OR LabelText IS NULL)" & vbCr & _
      "        AND GrpOrder = 3 " & vbCr & _
      "        AND GrpOrder IS NOT NULL" & vbCr & "       )" & vbCr & _
      "  FROM GrpOrders" & vbCr & _
      " ORDER " & vbCr & _
      "    BY GrpOrder DESC, LabelText DESC;"

      Dim rs
      Set rs = .Execute(Sql)

      ' Remove "AND GrpOrder IS NOT NULL"
      Sql = Replace$(Sql, "AND GrpOrder IS NOT NULL", "")

      Dim rs2
      Set rs2 = .Execute(Sql)

      MsgBox _
          "Include 'AND GrpOrder IS NOT NULL':" & vbCr & _
          rs.GetString(, , vbTab, vbCr, "<NULL>") & vbCr & _
          "remove 'AND GrpOrder IS NOT NULL':" & vbCr & _
          rs2.GetString(, , vbTab, vbCr, "<NULL>")


    End With
    Set .ActiveConnection = Nothing
  End With
End Sub
Community
  • 1
  • 1
onedaywhen
  • 55,269
  • 12
  • 100
  • 138
  • Ironically, you missed the fact that using a NOT you already solved the case where GrpOrder = NULL: In this case GrpOrder = 3 will return false, therefore the whole thing will return true - so you can remove "GrpOrder IS NOT NULL" predicate. Another way to look at it: GrpOrder can't be 3 and NULL at the same time, so checking it is not null in this case is redundant. – Yanir Kleiman Jul 08 '11 at 10:48
  • @Yanir Kleiman: "GrpOrder can't be 3 and NULL at the same time, so checking it is not null in this case is redundant" -- you are wrong and I can prove it :) See the update to this answer. – onedaywhen Jul 08 '11 at 13:49
  • The "avoid Null" advice is TERRIBLE. Just about the worst thing you've ever written. – David-W-Fenton Jul 10 '11 at 02:21
  • @David-W-Fenton: In you defence of `NULL`, can you explain why I need to add the `GrpOrder IS NOT NULL` predicate to get the desired results? – onedaywhen Jul 11 '11 at 11:05
  • The SQL is more complicated than I'm willing to delve into. I'm disputing the blanket condemnation of the use of `Null` (i.e., "NULL in SQL should be avoided"), not any specific issue that comes from having Nulls in your resultset. – David-W-Fenton Jul 13 '11 at 18:17
  • @David-W-Fenton: Looking at the positive: the highest normal form is 6NF. 6NF is always achievable. If your implementation is in 6NF you will have no need for nullable columns. Then you simply have to avoid SQL constructs that generate nulls e.g. rather the use outer join, use UNION with a (non-null) default value where the null would be in the equivalent outer join. – onedaywhen Jul 14 '11 at 08:06
  • @David-W-Fenton: Looking at the negative: "certain boolean expressions — and therefore certain queries in particular — produce results that are correct according to three-valued logic but not correct in the real world" (Chris Date). 3VL defies conventional logic, being counter-intuitive, you just have to learn it by rote. But not many people do, so they make mistakes, even so-called SQL experts. I can point you to hundred of misstatements on Stackoverflow alone regarding null ("null + anything = false", "null + anything = null", etc). Complexity + ignorance = disaster. – onedaywhen Jul 14 '11 at 08:07
  • @David-W-Fenton: "The SQL is more complicated than I'm willing to delve into" -- genuine respect to you for saying so. But this kind of proves my above point about unnecessary complexity. The discussion is merely about comparisons involving two nullable values. It shouldn't be complicated at all! Yet null complicates matters enormously. OK, in this case matters are worse because -- sorry! -- this is Access SQL (ACE, Jet, whatever) and... – onedaywhen Jul 14 '11 at 09:04
  • a) it has an odd boolean type `YESNO` which in theory is not nullable (a `YESNO` column can be declared NULLable, will not error when updating to null but will sliently change to a null to `FALSE`) yet can produce nulls (e.g. in an outer join); – onedaywhen Jul 14 '11 at 09:04
  • b) its null has odd behaviour e.g. null is treated as a data value (e.g. will be updated by `CASCADE` referential actions) -- and always the same data value (e.g. can't cast a null to a type, as you can in Standard SQL) -- but is also a logical value (e.g. `SELECT 0 + NULL IS NULL;` returns `TRUE`!!); – onedaywhen Jul 14 '11 at 09:04
  • c) has no meaningful spec (with which to identify bugs in the first place) and one cannot interest the Access Team in fixing bugs (Alan Browne has documented bugs with `YESNO` for years, have they been fixed yet?)... – onedaywhen Jul 14 '11 at 09:05
  • In the specific case in question, it defies even three value logic (even Access's own 3VL) to have to test `GrpOrder = 3` *and* `GrpOrder IS NOT NULL` because `3 IS NOT NULL` even with Access's somewhat quirky null and quirky boolean. On a practical note, if you can't avoid Access what can you do in such cases? Avoiding null would solve the problem in this case (and many, many other cases!) – onedaywhen Jul 14 '11 at 09:05
  • Imperfect implementations of NULL in certain database engines does not constitute a coherent argument against NULL on principle. – David-W-Fenton Jul 15 '11 at 18:58
  • 1
    Secondly, expecting NULL to behave differently than NULL is designed to behave is the principle reason why people have trouble with it. They expect that they can compare it to known values. That's just pilot error, not a problem with NULL, which logically must behave the way it is defined to behave. – David-W-Fenton Jul 15 '11 at 18:59
  • @David-W-Fenton: I agree it "must behave the way it is defined to behave" (though I strongly dispute your use of the word "logically") but the HUGE problem we have is that, unlike SQL, for the Access Database Engine (ACE, Jet, whatever) it has not been defined in any meaningful way. We are not privy to the design and with no specs, every bug becomes a feature. To extend your analogy, we're trying to pilot a plane with no manual (product specs) nor even the laws of aerodynamics (Relational model, SQL Standards, etc) as bases... and you claim that isn't a recipe for an air disaster?! – onedaywhen Jul 18 '11 at 08:24
  • @David-W-Fenton: "Imperfect implementations of NULL in certain database engines does not constitute a coherent argument against NULL on principle" -- perhaps not, but it certainly adds to the case against e.g. "...even the product designers can't get NULL down correct so what hope do us mere mortals have...?" – onedaywhen Jul 18 '11 at 08:26
  • There are lots of things in all sorts of database engines that only incompletely implement the theoretical standards (updatable views are the canonical example -- theoretically, all should be updatable; in reality, there is no db engine on earth that manages to reach that lofty goal). Database engines have long lives and things get implemented early on with imperfect implementations of theoretical concepts. Going forward, the designers of the db engine have to make choices about compatibility and other side effects of changes to basic behavior. (continued in next comment) – David-W-Fenton Jul 20 '11 at 21:31
  • Just yesterday a client of mine encountered just such a side effect of a change. We've discussed ANSI 92 mode and Access quite often, and the various data access modes that use the different LIKE wildcards. This client had an old database and had a query with a LIKE condition that used to work with the "Merge with Word" toolbar button. Now in A2007, it doesn't work. Why? Because Word 2007 use OLEDB, so `LIKE Fent*" returns nothing -- I've had her change it to `ALIKE Fent%` so it can work in both Access (with default ANSI 89 mode) and in Word Merge (using ANSI 92). (continued in next comment) – David-W-Fenton Jul 20 '11 at 21:33
  • Why does Word 2007 use OLEDB and not give the user a choice of data access method? I dunno. Word 2003 pops up a dialog allowing you to choose (and ODBC works with the old ANSI 89 syntax), but Word 2007 doesn't give the user a choice -- it just breaks. This is an end-user tool (a toolbar/ribbon button that automates a complex process) and in my opinion, whatever the desirability goals of getting past Access's ANSI 89 default, doing it this way just breaks things for people who are not equipped to figure out what went wrong. (continued...) – David-W-Fenton Jul 20 '11 at 21:35
  • What would be the result if Microsoft updated Jet/ACE to handle Nulls the way you prefer they be handled? I don't know. Maybe everything would still work. On the other hand, it would likely break queries that inadvertently depended on the "incorrect" implementation. All db engines have these kinds of issues because all db engines are particular implementations of theoretical constructs that do not actually exist in reality (and may never do so, in fact). Do other db engines get closer to the theoretical standard? Perhaps. But many of those have the advantage a shorter legacy to maintain. – David-W-Fenton Jul 20 '11 at 21:38
  • "All db engines have these kinds of issues... Do other db engines get closer to the theoretical standard?" -- I realize yours are rhetorical but consider SQL Server, it has [compatibility levels that can be set in SQL code](http://msdn.microsoft.com/en-us/library/ms188048.aspx), has a [`SET ANSI_NULLS`](http://msdn.microsoft.com/en-us/library/ms188048.aspx) at the batch level, has had three major releases (in SQL language terms) since its year 2000 release and has the 'courage' to deprecate features is favour of Standards ("In a future version of SQL Server, ANSI_NULLS will always be ON"). – onedaywhen Jul 21 '11 at 09:50
  • @David-W-Fenton: I assume by "theoretical standards" you are referring to what I would term 'mathematical foundation'. The Relational Model does indeed have such foundation and yes view updating has been solved on paper. However, SQL NULLs (Standard or otherwise) have no such foundation but [Wikipedia](http://en.wikipedia.org/wiki/Three-valued_logic#Kleene_logic) informs me of Kleene. So I'll merely in passing mention complexity: the equivalent boolean truth table has only four rows rather than nine. – onedaywhen Jul 21 '11 at 10:29
  • But from a practical (rather than theoretical) point of view, where's the value in a language feature that is complex (leads to bugs and perplexed coders) and counterintuitive (as regards convention logic, which even young children can understand thanks to natural language) when we have alternatives (e.g. the Closed World Assumption)? – onedaywhen Jul 21 '11 at 10:31
  • 1
    Null better represents real-world data. It's easier to work with than the alternatives, which involve manufacturing fake data. I'm done here. – David-W-Fenton Jul 22 '11 at 22:06
1

First of all the second IIF is redundant - "IIF(X, True, False)" can always be replaced by "X".

Apart from that, the logic of the select is "where GrpOrder = 3 and LabelText="Totals", OR GrpOrder <> 3".

That is the same as saying "where LabelText="Totals" OR GrpOrder <> 3", hence:

WHERE [GrpOrder] <> 3 OR [LabelText]="Totals"

*I don't remember if access uses <> or != for inequality, so whichever works.


Edit:

We have 4 cases in total:

GrpOrder = 3 and LabelText = "Totals" => accept

GrpOrder = 3 and LabelText <> "Totals" => don't accept

GrpOrder <> 3 and LabelText = "Totals" => accept

GrpOrder <> 3 and LabelText <> "Totals" => accept

The only case we do not accept is when GrpOrder = 3 and LabelText<> "Totals", which is the same as saying we accept all rows where GrpOrder <> 3 (bottom two rows) or LabelText="Totals" (first and third row). Row 2 is the only one that is not accepted.

Yanir Kleiman
  • 377
  • 1
  • 8
  • I'm not sure that's correct. My interpretation of these nested iif's are that they're trying to say "give me all records, with the following exception. I don't want any records where GrpOrder=3 except where GrpOrder=3 and LabelText="Totals". (I didn't write this, I inherited it) – Scott Silvi Jul 07 '11 at 23:15
  • Your interpretation is the logical complement of what I wrote. I'll edit the answer to make it clearer. – Yanir Kleiman Jul 07 '11 at 23:21
  • There's probably some Access quirk I'm unaware of (plus there are imbalanced parens in the question so I can't e sure) but reading this from a _SQL code_ point of view it seems you've missed a case: when `GrpOrder = 3` and `LabelText IS NULL` then the OP's predicate will evaluate to `FALSE` whereas yours will evaluate to `NULL`. While it is true that in SQL DML the effect will be the remove the row from the resultset, if this was used in SQL DDL (e.g. a `CHECK` constraint) or a calculated column then the effect would be to allow the update to succeed when it should fail. – onedaywhen Jul 08 '11 at 07:59
  • Doh! I missed the obvious case when `GrpOrder IS NULL` and `LabelText IS NULL`: in SQL DML (e.g. in the `WHERE` clause of a regular query) your predicate will remove the row whereas it would be retained by the OP's. – onedaywhen Jul 08 '11 at 08:20
  • "I don't remember if access uses <> or != for inequality" -- sounds like you didn't test this, hence the two cases you missed I suppose. Access uses `<>`, BTW. – onedaywhen Jul 08 '11 at 08:21
  • I didn't test it indeed, and never claimed to have tested it either. But even if I would, I wouldn't automatically assume both of these columns can contain NULL. NULL can be dealt with easily by adding ISNULL([GrpOrder]) OR ... in the beginning of the WHERE clause - assuming you want to return rows where GrpOrder = NULL in the first place. Also note that LabelText = NULL will not affect the correctness of the solution above. – Yanir Kleiman Jul 08 '11 at 10:31
  • "I wouldn't automatically assume both of these columns can contain NULL" -- Why would you automatically assume both of these columns can never contain NULL?! – onedaywhen Jul 08 '11 at 13:51
  • "NULL can be dealt with easily by adding ISNULL([GrpOrder]) OR..." -- I don't think it is as easy as that but I'm prepared for you to prove me wrong :) – onedaywhen Jul 08 '11 at 13:51
  • "assuming you want to return rows where GrpOrder = NULL" -- best not to assume and to get it to work the same as the OP's. – onedaywhen Jul 08 '11 at 13:52
  • "Also note that LabelText = NULL will not affect the correctness of the solution above" -- incorrect and I covered this in my first comment: when `GrpOrder = 3` and `LabelText IS NULL` then the OP's predicate will evaluate to FALSE whereas yours will evaluate to NULL. – onedaywhen Jul 08 '11 at 14:04
0

I don't want any records where GrpOrder=3 except where GrpOrder=3 and LabelText="Totals".

where GrpOrder <> 3 or (GrpOrder = 3 and LabelText="Totals")
JeffO
  • 7,957
  • 3
  • 44
  • 53