Skip to main content

SQL or CSS — Which Is the Better Language?

·11 mins
Michael Hönnig
Author
Michael Hönnig
Until autumn 2027 only available for consulting engagements with a small number of hours – remote, in German and English.

Recently, an article went around on X that essentially claimed CSS is a database. The article itself struck me as AI-generated slop, and of course CSS is not a database.

But the thought stuck with me anyway.

After all, SQL and CSS really do share something essential: both are declarative languages. So we mainly describe what we want, and leave it to a system to decide how to arrive at that result.

And yet I know many backend developers who write complex SQL queries without any trouble, yet despair over CSS at the mere question of why an element is five pixels too wide.

Myself included.

Why is that?

SQL and CSS Are Surprisingly Similar at First
#

Take a somewhat more interesting SQL query — the most expensive product of each active customer’s latest order:

SELECT DISTINCT ON ("c"."id") "c"."name", "p"."name" AS "product_name", "oi"."quantity", "p"."price"
  FROM "customer" AS "c"
  JOIN "order" AS "o" ON "o"."customer_id" = "c"."id"
  JOIN "order_item" AS "oi" ON "oi"."order_id" = "o"."id"
  JOIN "product" AS "p" ON "p"."id" = "oi"."product_id"
 WHERE "c"."status" = 'ACTIVE'
   AND "o"."order_date" = (SELECT MAX("o2"."order_date")
                             FROM "order" AS "o2"
                            WHERE "o2"."customer_id" = "c"."id")
 ORDER BY "c"."id", "p"."price" DESC;

Whether the query is performant or not is not the topic here. My point is, I do not describe how the database is supposed to walk through its data structures. I only say which data I want.

The database itself decides whether to use an index, for example, to run a sequential scan, or which join order makes sense.

CSS looks similar at first:

.orders:has(.order[data-status="pending"]) {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr));
    gap: var(--space-m, 1rem);
    container-type: inline-size;
}
.orders > .customer.active .order:last-child .most-expensive {
    font-weight: bold;
    font-size: calc(1rem + 0.25cqi);
}
@container (min-width: 48rem) {
    .orders > .customer.active .order:last-child .most-expensive {
        grid-column: span 2;
    }
}

Here, too, I do not describe how the browser is supposed to walk through the DOM tree. Instead, I formulate a rule:

For all elements this selector matches, this property shall apply.

You can even draw some rough analogies:

SQLCSS
Table / relationDOM
WHERESelector
Join conditionRelationships between elements in selector
Result setSet of matching elements
Query plannerStyle and layout engine
Declarative queryDeclarative style rule

Of course, this analogy is limited. CSS is not a relational query system, and SQL is not a layout language.

But both demand a similar shift from developers compared to imperative programming languages:

Describe the desired result, not the algorithm that leads there.

So why does SQL feel so much more controllable to many backend developers?

SQL Is Usually Surprisingly Local
#

With a normal SQL query, I can reason about what happens in a fairly local way.

When I replace

WHERE "status" = 'ACTIVE'

with

WHERE "status" IN ('ACTIVE', 'PENDING')

the effect is pretty obvious.

Of course, the query can get complicated. Ten tables may be involved, common table expressions, window functions, subqueries, and aggregations. But the logic that determines the result set is largely right in front of me.

CSS works differently. Take a seemingly harmless property like:

display: flex;

That does not just change a visible property of an element. It simultaneously changes the layout context of its children.

Or:

position: relative;

The element may look exactly the same as before. At the same time, however, it can become the reference point for a completely different, absolutely positioned element.

Similarly, overflow, transform, contain, or certain layout properties can affect how other elements are computed or rendered.

CSS semantics, unlike those of SQL, are unfortunately often anything but local.

With CSS, the Real Problem Only Starts After the Selector
#

Perhaps the comparison with SQL leads to a typical misunderstanding.

With SQL, selecting the data is normally already an essential part of the result.

With CSS, a selector answers only the first question:

Which elements does this rule apply to?

Only then does it really get going.

Multiple rules can apply to one property. So the cascade has to determine which one wins. That involves, among other things, origin, cascade layers, specificity, and order.

Then there is inheritance.

Then relative values have to be computed.

And finally, the browser has to determine the layout, where sizes and positions in turn can depend on parent elements, siblings, content, fonts, viewport, flexbox, grid, or container queries.

Very roughly, CSS thus goes through something like:

Selection
Cascade
Inheritance
Computed Values
Layout
Rendering

That is considerably more than:

Find all elements with the class customer.

The selector is more like the CSS counterpart of WHERE. It is not yet the actual computation of the result.

But Is SQL Really That Local?
#

Up to this point, one might reach the simple conclusion:

SQL is well structured and local, CSS is full of hidden dependencies.

But it is not quite that simple.

Because when I think about what I usually mean when I say that I “know SQL”, I blank out quite a large part of what can happen in real databases.

Take a trigger.

I execute:

UPDATE "customer"
SET "status" = 'CANCELLED'
WHERE "id" = 123;

If I look at only this statement, it seems perfectly clear what happens.

But there may be a trigger on "customer".

  • It writes an audit entry.
  • It may update yet another table.
  • That may fire another trigger.
  • Or a function is called that does entirely different things.

Suddenly I have exactly the problem that annoys me about CSS:

I change something here – and something happens somewhere else that is not visible at all at this point.

So SQL can definitely become non-local.

When SQL Starts to Feel Like CSS
#

Triggers are probably the most obvious example of this.

But not the only one.

  • Foreign keys with ON DELETE CASCADE can delete further records when a record is deleted.
  • Views can hide a considerable amount of logic behind a seemingly simple object.
  • Functions can execute further logic inside a query.

Generated values, constraints, and other database mechanisms also influence the behavior of a statement, without being fully visible in the statement itself.

And then there are stored procedures. That is where we often even leave the purely declarative world.

Oracle uses PL/SQL for this, for example, PostgreSQL among others PL/pgSQL. That adds variables, conditions, loops, exceptions, and explicit control flow.

The interesting thing about this is:

When backend developers say they are good with SQL, they usually do not automatically mean that they also consider 20,000 lines of PL/SQL with stored procedures and nested trigger chains to be clear, easily understandable software.

Quite the opposite.

Triggers have a bad reputation on many development teams precisely because they hide behavior.

So perhaps backend developers are not as tolerant of non-locality as the comparison with CSS initially suggests.

We just no longer necessarily call the problematic parts “SQL”.

Perhaps We Compare Nice SQL with Evil CSS
#

That makes the comparison somewhat unfair.

When we think of SQL, we usually think of something like:

SELECT ...
FROM ...
WHERE ...

When we think of CSS problems, by contrast, we think of a large application with hundreds of components, browser defaults, multiple stylesheets, inheritance, cascade, flexbox, grid, responsive design, and a rule that someone marked with !important three years ago.

So we may be comparing:

small, declarative SQL

with:

a complex CSS system.

The fairer comparison might be:

a single SQL query with a single CSS rule

and:

a large database with views, constraints, functions, stored procedures, and triggers with a large CSS codebase.

And suddenly the two worlds are not that far apart after all.

  • In both cases, a local change can be influenced by rules defined somewhere else.
  • In both cases, you have to know a larger context to reliably predict the result.
  • And in both cases, you eventually start using tools to find out what is actually happening.

With SQL, that is EXPLAIN ANALYZE, for example.

With CSS, it is the browser developer tools with computed styles, layout views, and the question of which rule is actually winning.

Still, There Is One Important Difference
#

Still, that does not fully resolve the difference between SQL and CSS.

An SQL query essentially describes an operation on data.

For a given database state, it has a defined meaning. The query planner may use completely different strategies, as long as they produce the same result.

Whether PostgreSQL uses an index scan or a sequential scan should not change the functional result of my query.

The query planner is thus mainly a performance concern.

With CSS, “execution” is much more closely tied to the actual result.

The size of an element can depend, for example, on how much space its siblings need. Those in turn depend on their content. The content depends on the font used. And another container can in turn activate different styles via a container query.

The layout itself is part of the result.

CSS thus does not simply describe a query against a DOM tree. It describes a system of rules and dependencies from which the browser has to compute a concrete visual state.

Modern layout systems like flexbox and grid therefore sometimes feel more like constraint programming.

I say, roughly speaking:

This element may grow, this one may shrink, that one should be at least as large as its content, the columns should share the available space, and different rules apply above a certain container size.

And the browser solves this system for me.

SQL Has an Escape Hatch
#

There is another difference that I find particularly interesting.

When a database’s declarative model becomes inconvenient, there is almost always a way out.

  • I can write a stored procedure.
  • I get variables.
  • I get IF.
  • I get loops.

In the extreme case, I can therefore effectively say:

Forget the elegant relational solution. I will just program it now.

Within CSS, this option does not exist.

Of course I can use JavaScript. But then I have left CSS.

CSS itself forces me with remarkable consistency to keep expressing the problem declaratively.

Modern CSS features like custom properties, calc(), grid, container queries, or :has() make the language ever more powerful. But they do not turn it into a classic imperative programming language.

Perhaps that is exactly one reason why CSS is sometimes so frustrating for developers with an imperative background.

SQL eventually lets us step out of the declarative paradigm.

CSS does not.

Can Backend Developers Really Do SQL Better Than CSS?
#

That brings me back to my original question.

Why can so many backend developers do SQL, but supposedly not CSS?

Perhaps the question itself is wrong.

Most backend developers probably master a relatively benign subset of SQL:

  • SELECT, JOIN, and WHERE
  • Aggregations with GROUP BY
  • INSERT, UPDATE, and DELETE

That roughly corresponds to the part of the language where cause and effect are still fairly easy to follow locally.

But most developers drop out when it comes to stored procedures, triggers, CTE queries, and window functions.

So once a database is full of triggers, hidden side effects, and extensive PL/SQL or PL/pgSQL programs, many developers react remarkably similarly to a large legacy CSS stylesheet:

Where does this value come from?

Why is this happening?

Who added this rule?

What breaks if I change this?

Perhaps CSS is not fundamentally harder to understand than SQL after all.

Perhaps with CSS we just reach the point where declarative rules interact much sooner.

And Which Language Is Better?
#

That leaves the question from the title.

SQL or CSS — Which Is the Better Language?

Of course, the question is nonsense. Both solve completely different problems.

But if I had to compare them anyway, I would give SQL an advantage in local comprehensibility. A single query can usually be viewed in fairly isolated fashion. A single CSS block, by contrast, depends on its surroundings more quickly.

CSS, on the other hand, is impressively consistent in being declarative.

A browser can apply the same stylesheet on a smartphone, a laptop, or a huge screen, react to content whose length nobody knew when the CSS was written, take different fonts into account, and compute a layout from all of that.

If you wanted to program all of that imperatively, the code would probably be considerably worse than the CSS we complain about.

And perhaps that is the real insight from the comparison:

CSS may not be difficult because it is a bad programming language.

It is difficult because we often try to read it like a programming language, while the browser is actually solving a system of rules and constraints.

SQL long ago got us used to telling the computer what we want.

With CSS, we apparently find it harder to also trust it to figure out how to get there.

This article was translated from German with AI assistance.