DB Olympics

Queries · q102 · Chief Information Security Officer · week 1, night 1

Privileged roles per merchant: grant size and dormancy

“Which roles across our merchants carry the most permissions, how many users currently hold each role, and how many of those users look dormant (no recent login) — a first read on access-governance hygiene?”

Manager · asked the questionHenrik AaltoChief Information Security Officer. Joined from a cloud provider's security team. Several enterprise deals depend on passing security reviews.
Analyst · planned the reportNoah FischerSystems thinker: maps how one process feeds the next before measuring anything.
Engineer · wrote the SQLInesThinks in window functions: ranks, running totals, LAG/LEAD for comparisons, percent-of-total.

Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told

  1. Henrik Aalto writes to Noah Fischer

    The manager, at the start of week 1

    Subject: Where I'd like your help first — account security snapshot

    Hi Noah,

    Good to have you working with me this week. I'm still building my mental map of the business, so let's start broad and sharpen later. Here's what's on my mind, roughly in priority order:

    1. How exposed are we on sign-in security today? I want to understand how merchant staff and shoppers are authenticating — are we seeing risky patterns like repeated failed logins, credential-stuffing-type behavior, or logins from unusual places? I need a feel for the baseline before I can say whether we have a problem.
    2. How strong is our multi-factor authentication posture? I'd like to know roughly how many of our merchants' staff (and ideally shoppers, if meaningful) actually have MFA set up and active, versus how many are relying on password-only access. This matters because it's one of the first things enterprise security reviewers ask about, and I need to know whether we can answer confidently or whether it's a gap we need to close.
    3. Who holds powerful access, and is it still appropriate? I'm interested in how permissions and roles are granted and whether there's good hygiene around it — e.g., are there dormant privileged accounts, or grants that nobody seems to be reviewing. This feeds directly into access-control questions in security audits.
    4. How healthy are our API keys and service accounts? These are often the weakest link — long-lived tokens that nobody rotates. I'd like a sense of how many are active, how old they are, and whether any look abandoned or over-privileged.
    5. Can we actually reconstruct "who did what, when" if asked? I want to understand how complete and reliable our audit trail is in practice — are sensitive actions consistently logged, and would we be able to produce a credible record if a merchant or regulator asked us to investigate an incident?

    What I need this for: I'm walking into enterprise deals where security questionnaires will ask us point-blank about authentication strength, access governance, credential hygiene, and auditability. I need an honest, evidence-based picture — good or bad — so I can brief the CEO and sales on what we can credibly claim today versus what needs fixing before the next review cycle.

    Given the slot budget, let's pick whichever 3 of these will give us the most defensible starting picture this week, and we can go deeper on the rest next week.

    Thanks, Henrik

  2. Noah Fischer plans the night

    The analyst · week 1, night 1, request 3 of 3

    Henrik flagged 5 areas but asked us to pick the 3 that give the most defensible starting picture this week, saving the rest for next week. I'm starting with his top 3 stated priorities (sign-in security baseline, MFA posture, privileged access hygiene) and deliberately deferring API-key/service-account hygiene and audit-trail completeness to night 2+, since those depend on first understanding how identity and access actually work here (systems view: authentication -> MFA -> authorization all feed each other; keys/audit are downstream concerns I'll size up once I see how messy or clean the core picture is).

    Report 1 establishes a platform-wide time baseline for login behavior so any later "spike" claims have something to compare against. Report 2 gives a per-merchant MFA adoption view, ordered to surface the worst gaps first since that's the actionable finding for a security head walking into enterprise reviews. Report 3 looks at role/permission grants per merchant to see how concentrated and stale ("dormant") privileged access is — the classic access-governance audit question. All three are single-grain, independent, and should each stand on their own without needing follow-up queries to simply read.

    Privileged roles per merchant: grant size and dormancy
    Question
    Which roles across our merchants carry the most permissions, how many users currently hold each role, and how many of those users look dormant (no recent login) — a first read on access-governance hygiene?
    One row per
    tenant and role
    Columns
    • tenant name
    • role name
    • is_system role (built-in vs custom)
    • permission count granted to role
    • users currently assigned this role (via user_roles)
    • of those, users with status = active
    • of those, users dormant (status = active but last_login_at is null or before 2025-10-05, i.e. no login in the trailing ~90 days)
    • most recent last_login_at among assigned users
  3. Ines writes the SQL

    The engineer · 1 dry run on the empty schema, then the submission

    Dry run 1 · STATUS: COMPILES (dry run on the empty schema, no data read)
    WITH role_perm_counts AS (
      SELECT role_id, COUNT(*) AS permission_count
      FROM role_permissions
      GROUP BY role_id
    ),
    role_users AS (
      SELECT ur.role_id,
             u.id AS user_id,
             u.status,
             u.last_login_at
      FROM user_roles ur
      JOIN users u ON u.id = ur.user_id AND u.tenant_id = ur.tenant_id
      WHERE u.deleted_at IS NULL
    ),
    role_user_agg AS (
      SELECT role_id,
             COUNT(*) AS users_assigned,
             COUNT(*) FILTER (WHERE status = 'active') AS users_active,
             COUNT(*) FILTER (WHERE status = 'active' AND (last_login_at IS NULL OR last_login_at < DATE '2025-10-05')) AS users_dormant,
             MAX(last_login_at) AS most_recent_login
      FROM role_users
      GROUP BY role_id
    )
    SELECT
      t.name AS tenant_name,
      r.name AS role_name,
      r.is_system,
      rpc.permission_count,
      rua.users_assigned,
      rua.users_active,
      rua.users_dormant,
      rua.most_recent_login,
      COUNT(*) OVER () AS total_qualifying_roles
    FROM roles r
    JOIN tenants t ON t.id = r.tenant_id
    JOIN role_perm_counts rpc ON rpc.role_id = r.id
    JOIN role_user_agg rua ON rua.role_id = r.id
    ORDER BY rpc.permission_count DESC, rua.users_assigned DESC, t.name, r.name
    LIMIT 50

    “Query compiles cleanly with the expected columns. Submitting.”

    Submitted · the SQL of the last dry run, unchanged · Submitted to tonight's batch. Your work on this request is done.

    “Submitted. The report returns one row per tenant/role (roles with at least one permission and one currently-assigned, non-deleted user), ordered by permission count then assignment count, with tenant/role as final tie-breaks, capped at 50 rows. I included a total_qualifying_roles window-function column so Noah can see the full population size without needing a separate count query.”

  4. The report comes back overnight

    As Noah Fischer saw it, from the small dataset the agents worked on

    tenant_name                   | role_name | is_system | permission_count | users_assigned | users_active | users_dormant | most_recent_login          | total_qualifying_roles
    ------------------------------+-----------+-----------+------------------+----------------+--------------+---------------+----------------------------+-----------------------
    Ampfeld Elektronik            | Owner     | true      |               26 |              2 |            2 |             0 | 2025-12-31 07:08:19.175557 |                    434
    Cragsley Outfitters           | Owner     | true      |               26 |              2 |            2 |             0 | 2025-12-31 14:00:26.683942 |                    434
    Marrowby & Vane               | Owner     | true      |               26 |              2 |            2 |             0 | 2025-12-26 07:41:36.208017 |                    434
    Alpine Loft                   | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-23 14:06:49.18011  |                    434
    Ampere & Co.                  | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-25 08:48:33.910648 |                    434
    Atelier Nouveau               | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 08:34:38.260986 |                    434
    Aurum & Ash                   | Owner     | true      |               26 |              1 |            1 |             1 | 2024-12-26 09:20:33.907508 |                    434
    Ballycorran Industrial Supply | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-15 09:55:50.106232 |                    434
    Barkley & Co.                 | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 08:57:42.128574 |                    434
    Basecamp Nord                 | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 07:38:51.397011 |                    434
    Bijoux Lune                   | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 08:21:16.785713 |                    434
    Brennquell Kaffee             | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 06:04:23.969469 |                    434
    Brewline Coffee               | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-19 08:38:46.510976 |                    434
    Brick & Bobbin                | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 08:48:23.696749 |                    434
    Buchstabe                     | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 08:01:15.575688 |                    434
    Bytefield                     | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 07:59:26.126407 |                    434
    Castell Fasteners             | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-05 16:09:06.418974 |                    434
    Chocolaterie Mireille         | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-23 06:03:42.60224  |                    434
    Cinder Peak Sports Co.        | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-26 17:55:00.698728 |                    434
    Circuit Harbor                | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-18 08:45:47.693554 |                    434
    Coastline Interiors           | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 06:40:46.358964 |                    434
    Copper Lane                   | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 15:41:24.665014 |                    434
    Courtside Collective          | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 07:18:11.26492  |                    434
    Deskline Supplies             | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 07:41:34.360103 |                    434
    Dewdrop Studio                | Owner     | true      |               26 |              1 |            1 |             0 | 2025-10-09 09:44:11.78363  |                    434
    Driftwood Home                | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 06:34:26.176787 |                    434
    Feather & Fin                 | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-21 15:01:20.730905 |                    434
    Fell & Fjell                  | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 06:33:59.909025 |                    434
    Fennel & Flint                | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 10:30:00.904438 |                    434
    Fern & Fable                  | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 09:50:07.404579 |                    434
    Fjordline Wear                | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-24 07:08:21.240599 |                    434
    Folio & Fern                  | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-25 07:53:09.345969 |                    434
    Glasshouse Grooming           | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-29 17:09:48.066091 |                    434
    Goldkind                      | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-24 08:49:57.98021  |                    434
    Gratlinie Outdoor             | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 08:18:14.474838 |                    434
    Greyfriars Supply             | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 12:33:46.367033 |                    434
    Grind Collective              | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-22 07:03:34.101547 |                    434
    Halden Boots                  | Owner     | true      |               26 |              1 |            1 |             1 | 2025-10-03 08:05:13.116482 |                    434
    Hanse Industriebedarf         | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-26 07:52:54.846781 |                    434
    Harbour Swim                  | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 07:59:41.720572 |                    434
    Hautnah                       | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-19 07:02:22.875747 |                    434
    Haven Linen                   | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-28 08:22:05.259461 |                    434
    Honeycomb Larder              | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-29 15:14:47.363465 |                    434
    Hound Hamper                  | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-30 09:06:03.997591 |                    434
    Hus & Hem                     | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 07:45:02.163092 |                    434
    Inkwell & Quire               | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-12 08:45:44.918052 |                    434
    Juniper Apothecary            | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-24 07:13:45.060943 |                    434
    Kabelwerk                     | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-26 08:47:56.890307 |                    434
    Kaffeekontor Nord             | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-31 09:24:55.839162 |                    434
    Kayak Corner                  | Owner     | true      |               26 |              1 |            1 |             0 | 2025-12-29 08:22:47.288399 |                    434
    (50 rows)
  5. What came of it

    The memo and the queries that followed; links found by title, so inferred

    Later queries that build on this report:

  6. It becomes benchmark query q102

    Changed only where the benchmark needs the same result on every system

    • spellingfetch first
    • determinismtie-breaker: one row per role
    • made explicita selected t.col gets AS col, its standard name made explicit (9×)
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results (5×)
    • placeholderfetch first
    The benchmark text, against the submitted SQL
    WITH role_perm_counts AS (  SELECT role_id, COUNT(*) AS permission_count  FROM role_permissions  GROUP BY role_id),role_users AS (  SELECT ur.role_id,  SELECT ur.role_id AS role_id,         u.id AS user_id,         u.status,         u.last_login_at         u.status AS status,         u.last_login_at AS last_login_at  FROM user_roles ur  JOIN users u ON u.id = ur.user_id AND u.tenant_id = ur.tenant_id  WHERE u.deleted_at IS NULL),role_user_agg AS (  SELECT role_id,         COUNT(*) AS users_assigned,         COUNT(*) FILTER (WHERE status = 'active') AS users_active,         COUNT(*) FILTER (WHERE status = 'active' AND (last_login_at IS NULL OR last_login_at < DATE '2025-10-05')) AS users_dormant,         MAX(last_login_at) AS most_recent_login  FROM role_users  GROUP BY role_id)SELECT  t.name AS tenant_name,  r.name AS role_name,  r.is_system,  rpc.permission_count,  rua.users_assigned,  rua.users_active,  rua.users_dormant,  rua.most_recent_login,  r.is_system AS is_system,  rpc.permission_count AS permission_count,  rua.users_assigned AS users_assigned,  rua.users_active AS users_active,  rua.users_dormant AS users_dormant,  rua.most_recent_login AS most_recent_login,  COUNT(*) OVER () AS total_qualifying_rolesFROM roles rJOIN tenants t ON t.id = r.tenant_idJOIN role_perm_counts rpc ON rpc.role_id = r.idJOIN role_user_agg rua ON rua.role_id = r.idORDER BY rpc.permission_count DESC, rua.users_assigned DESC, t.name, r.nameLIMIT 50ORDER BY rpc.permission_count DESC NULLS FIRST, rua.users_assigned DESC NULLS FIRST, t.name NULLS LAST, r.name NULLS LAST, r.id NULLS LAST{{fetch_first(50)}}
    WITH role_perm_counts AS (
      SELECT role_id, COUNT(*) AS permission_count
      FROM role_permissions
      GROUP BY role_id
    ),
    role_users AS (
      SELECT ur.role_id AS role_id,
             u.id AS user_id,
             u.status AS status,
             u.last_login_at AS last_login_at
      FROM user_roles ur
      JOIN users u ON u.id = ur.user_id AND u.tenant_id = ur.tenant_id
      WHERE u.deleted_at IS NULL
    ),
    role_user_agg AS (
      SELECT role_id,
             COUNT(*) AS users_assigned,
             COUNT(*) FILTER (WHERE status = 'active') AS users_active,
             COUNT(*) FILTER (WHERE status = 'active' AND (last_login_at IS NULL OR last_login_at < DATE '2025-10-05')) AS users_dormant,
             MAX(last_login_at) AS most_recent_login
      FROM role_users
      GROUP BY role_id
    )
    SELECT
      t.name AS tenant_name,
      r.name AS role_name,
      r.is_system AS is_system,
      rpc.permission_count AS permission_count,
      rua.users_assigned AS users_assigned,
      rua.users_active AS users_active,
      rua.users_dormant AS users_dormant,
      rua.most_recent_login AS most_recent_login,
      COUNT(*) OVER () AS total_qualifying_roles
    FROM roles r
    JOIN tenants t ON t.id = r.tenant_id
    JOIN role_perm_counts rpc ON rpc.role_id = r.id
    JOIN role_user_agg rua ON rua.role_id = r.id
    ORDER BY rpc.permission_count DESC NULLS FIRST, rua.users_assigned DESC NULLS FIRST, t.name NULLS LAST, r.name NULLS LAST, r.id NULLS LAST
    FETCH FIRST 50 ROWS ONLY
    1 ms10 ms100 msHyper7.9 msDuckDB7.8 msGizmoSQL15.3 msclickhouse_brew75.2 msFirebolt60.4 msDataFusion53.6 msClickHouse27.3 ms
  7. Everything, as recorded

    The agents' own records, as the role play wrote them; only the personal data of the person who ran it is redacted

    Ines's whole session, 7 turns
    • The request, as Ines received it
      Report request from Noah Fischer (analyst for the Chief Information Security Officer):
      
      Title: Privileged roles per merchant: grant size and dormancy
      Question: Which roles across our merchants carry the most permissions, how many users currently hold each role, and how many of those users look dormant (no recent login) — a first read on access-governance hygiene?
      Grain: one row per tenant and role
      Columns:
      - tenant name
      - role name
      - is_system role (built-in vs custom)
      - permission count granted to role
      - users currently assigned this role (via user_roles)
      - of those, users with status = active
      - of those, users dormant (status = active but last_login_at is null or before 2025-10-05, i.e. no login in the trailing ~90 days)
      - most recent last_login_at among assigned users
      Details: Source: roles joined to tenants; role_permissions grouped by role_id for permission count; user_roles joined to users for assignment counts and last_login_at. Exclude soft-deleted users (users.deleted_at is null). 'Dormant' = users.status = 'active' and (last_login_at is null or last_login_at < 2025-10-05, i.e. no login in the 90 days before the data cutoff 2025-12-31). Only include roles that have at least 1 permission and at least 1 assigned user (skip empty/unused roles for this first pass). Order by permission count descending, then users assigned descending, then tenant name, then role name as the final tie-break, capped at 50 rows — note how many qualifying role rows exist in total.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Ines calls dry_run
      WITH role_perm_counts AS (
        SELECT role_id, COUNT(*) AS permission_count
        FROM role_permissions
        GROUP BY role_id
      ),
      role_users AS (
        SELECT ur.role_id,
               u.id AS user_id,
               u.status,
               u.last_login_at
        FROM user_roles ur
        JOIN users u ON u.id = ur.user_id AND u.tenant_id = ur.tenant_id
        WHERE u.deleted_at IS NULL
      ),
      role_user_agg AS (
        SELECT role_id,
               COUNT(*) AS users_assigned,
               COUNT(*) FILTER (WHERE status = 'active') AS users_active,
               COUNT(*) FILTER (WHERE status = 'active' AND (last_login_at IS NULL OR last_login_at < DATE '2025-10-05')) AS users_dormant,
               MAX(last_login_at) AS most_recent_login
        FROM role_users
        GROUP BY role_id
      )
      SELECT
        t.name AS tenant_name,
        r.name AS role_name,
        r.is_system,
        rpc.permission_count,
        rua.users_assigned,
        rua.users_active,
        rua.users_dormant,
        rua.most_recent_login,
        COUNT(*) OVER () AS total_qualifying_roles
      FROM roles r
      JOIN tenants t ON t.id = r.tenant_id
      JOIN role_perm_counts rpc ON rpc.role_id = r.id
      JOIN role_user_agg rua ON rua.role_id = r.id
      ORDER BY rpc.permission_count DESC, rua.users_assigned DESC, t.name, r.name
      LIMIT 50
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: tenant_name, role_name, is_system, permission_count, users_assigned, users_active, users_dormant, most_recent_login, total_qualifying_roles
    • InesQuery compiles cleanly with the expected columns. Submitting.
    • Ines calls submit
      WITH role_perm_counts AS (
        SELECT role_id, COUNT(*) AS permission_count
        FROM role_permissions
        GROUP BY role_id
      ),
      role_users AS (
        SELECT ur.role_id,
               u.id AS user_id,
               u.status,
               u.last_login_at
        FROM user_roles ur
        JOIN users u ON u.id = ur.user_id AND u.tenant_id = ur.tenant_id
        WHERE u.deleted_at IS NULL
      ),
      role_user_agg AS (
        SELECT role_id,
               COUNT(*) AS users_assigned,
               COUNT(*) FILTER (WHERE status = 'active') AS users_active,
               COUNT(*) FILTER (WHERE status = 'active' AND (last_login_at IS NULL OR last_login_at < DATE '2025-10-05')) AS users_dormant,
               MAX(last_login_at) AS most_recent_login
        FROM role_users
        GROUP BY role_id
      )
      SELECT
        t.name AS tenant_name,
        r.name AS role_name,
        r.is_system,
        rpc.permission_count,
        rua.users_assigned,
        rua.users_active,
        rua.users_dormant,
        rua.most_recent_login,
        COUNT(*) OVER () AS total_qualifying_roles
      FROM roles r
      JOIN tenants t ON t.id = r.tenant_id
      JOIN role_perm_counts rpc ON rpc.role_id = r.id
      JOIN role_user_agg rua ON rua.role_id = r.id
      ORDER BY rpc.permission_count DESC, rua.users_assigned DESC, t.name, r.name
      LIMIT 50
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • InesSubmitted. The report returns one row per tenant/role (roles with at least one permission and one currently-assigned, non-deleted user), ordered by permission count then assignment count, with tenant/role as final tie-breaks, capped at 50 rows. I included a `total_qualifying_roles` window-function column so Noah can see the full population size without needing a separate count query.