1. Faro

Nivel 2 · Clientes que devolvieron algo

La solicitud

De Rachel Adams, Líder de Atención al Cliente

“Queremos encuestar a los clientes que han devuelto al menos un artículo. ¿Puedes listarlos, una vez cada uno, con sus nombres? No necesito los detalles de la devolución.”

Tu objetivo

Devuelve customer_id, first_name y last_name de todo cliente que tenga al menos una fila en returns (returns se enlaza con order_items, que se enlaza con orders, que se enlaza con customers). Cada cliente debe aparecer solo una vez.

  • Lee de la tabla customerspendiente
  • Lee de la tabla orderspendiente
  • Muestra exactamente customer_id first_name last_namependiente

Tu consulta

SQL

Cargando el editor…

    ResultadoVista previa de customers · presiona Ejecutar para ver el tuyo
    Vista previa de customers
    customer_idfirst_namelast_nameemailcity
    1OwenHudsonowen.hudson@icloud.comSan Francisco
    2DouglasMendozadmendoza69@gmail.comAtlanta
    3HenryBrookshbrooks@lakesidecafe.comLos Angeles
    4BrianGrahambgraham@bluecedarhomes.comSeattle

    Mote revisa tu resultado

    Presiona Ejecutar. Cada fila que cumple el objetivo se vuelve una luciérnaga en tu linterna.

    Tablas

    Datos de la tienda Harbor & Pine, listos para consultar.

    customers2,018 filas · 9 columnas
    • customer_idinteger
    • first_namevarchar
    • last_namevarchar
    • emailvarchar
    • cityvarchar
    • statevarchar
    • signup_datedate
    • segmentvarchar
    • referral_sourcevarchar
    Primeras 5 filas de customers
    customer_idfirst_namelast_nameemailcitystatesignup_datesegmentreferral_source
    1OwenHudsonowen.hudson@icloud.comSan FranciscoCA2022-06-02consumersearch
    2DouglasMendozadmendoza69@gmail.comAtlantaGA2022-06-02consumerNULL
    3HenryBrookshbrooks@lakesidecafe.comLos AngelesCA2022-06-04businessNULL
    4BrianGrahambgraham@bluecedarhomes.comSeattleWA2022-06-06businesspinterest
    5KyleJimenezkjimenez27@yahoo.comIndianapolisIN2022-06-07consumerNULL
    orders12,000 filas · 7 columnas
    • order_idinteger
    • customer_idinteger
    • order_datetimestamp
    • statusvarchar
    • channelvarchar
    • shipping_feedecimal
    • coupon_codevarchar
    Primeras 5 filas de orders
    order_idcustomer_idorder_datestatuschannelshipping_feecoupon_code
    10011252023-01-01 20:51:19completedweb0.00NULL
    10021562023-01-02 07:58:46completedweb0.00NULL
    1003222023-01-02 10:25:27completedmarketplace4.99WELCOME10
    1004482023-01-02 11:21:51completedmarketplace4.99WELCOME10
    1005822023-01-02 11:43:36completedweb0.00WELCOME10
    order_items30,338 filas · 6 columnas
    • order_item_idinteger
    • order_idinteger
    • product_idinteger
    • quantityinteger
    • unit_pricedecimal
    • discount_pctdecimal
    Primeras 5 filas de order_items
    order_item_idorder_idproduct_idquantityunit_pricediscount_pct
    11001121931.990.00
    21001154311.990.00
    31002154411.990.00
    410021941149.000.00
    51002193219.990.00
    returns1,673 filas · 5 columnas
    • return_idinteger
    • order_item_idinteger
    • returned_ondate
    • reasonvarchar
    • refund_amountdecimal
    Primeras 5 filas de returns
    return_idorder_item_idreturned_onreasonrefund_amount
    1142023-01-10damaged83.69
    2422023-01-10wrong_item19.99
    31052023-01-14wrong_item10.99
    4242023-01-16changed_mind26.99
    51262023-01-16late20.99
    products120 filas · 8 columnas
    • product_idinteger
    • namevarchar
    • categoryvarchar
    • subcategoryvarchar
    • unit_pricedecimal
    • unit_costdecimal
    • launched_ondate
    • is_activeboolean
    Primeras 5 filas de products
    product_idnamecategorysubcategoryunit_priceunit_costlaunched_onis_active
    101Vale Velvet LoveseatFurnitureSofas1659.00NULL2023-09-11true
    102Alder Oak Coffee TableFurnitureTables919.00503.502024-05-16true
    103Orchard Rattan Dining ChairFurnitureChairs789.00429.872023-05-27true
    104Upland Oak BookcaseFurnitureStorage429.00214.702022-12-20true
    105Avery Leather Bed FrameFurnitureBeds449.00232.702021-08-31true
    employees60 filas · 8 columnas
    • employee_idinteger
    • first_namevarchar
    • last_namevarchar
    • departmentvarchar
    • titlevarchar
    • manager_idinteger
    • hire_datedate
    • salaryinteger
    Primeras 5 filas de employees
    employee_idfirst_namelast_namedepartmenttitlemanager_idhire_datesalary
    1CatherineHaleExecutiveChief Executive OfficerNULL2016-03-01285000
    2RobertKimExecutiveChief Operating Officer12017-01-09235000
    3MeganCollinsSalesHead of Sales12018-04-16172000
    4ChloeGutierrezSalesAccount Executive32020-01-2172000
    5FrankBellSalesAccount Executive32023-05-2173000
    web_sessions40,000 filas · 7 columnas
    • session_idinteger
    • customer_idinteger
    • started_attimestamp
    • devicevarchar
    • sourcevarchar
    • pages_viewedinteger
    • convertedboolean
    Primeras 5 filas de web_sessions
    session_idcustomer_idstarted_atdevicesourcepages_viewedconverted
    1NULL2023-01-01 23:02:24mobilepaid1false
    2182023-01-01 15:07:49desktoppaid1false
    3NULL2023-01-01 16:55:46desktopdirect7false
    4NULL2023-01-01 12:43:28mobileorganic3false
    5NULL2023-01-01 13:06:21mobilesocial2false
    marketing_spend3,276 filas · 3 columnas
    • spend_datedate
    • channelvarchar
    • amountdecimal
    Primeras 5 filas de marketing_spend
    spend_datechannelamount
    2023-01-01paid492.99
    2023-01-01email37.38
    2023-01-01social200.53
    2023-01-02paid709.13
    2023-01-02email64.13

    La lección

    “Clientes que hicieron X” es un semi-join: conservar filas de una tabla cuando existe una fila relacionada en otra, sin traer las columnas de esa tabla. Hay tres formas de escribirlo, y no se comportan igual.

    JOIN crea duplicados

    SELECT c.customer_id, c.first_name
    FROM customers AS c
    JOIN orders AS o ON o.customer_id = c.customer_id
    JOIN order_items AS i ON i.order_id = o.order_id
    JOIN returns AS r ON r.order_item_id = i.order_item_id;

    Un cliente con tres devoluciones aparece tres veces. Podrías añadir DISTINCT, pero ese es el hábito de “esconder el fan-out” del capítulo 1. Aquí funciona y se rompe en silencio en cuanto alguien añade un SUM.

    EXISTS

    SELECT c.customer_id, c.first_name, c.last_name
    FROM customers AS c
    WHERE EXISTS (
      SELECT 1
      FROM orders AS o
      JOIN order_items AS i ON i.order_id = o.order_id
      JOIN returns AS r ON r.order_item_id = i.order_item_id
      WHERE o.customer_id = c.customer_id
    );

    EXISTS es verdadero si la subconsulta devuelve al menos una fila. Cada cliente se prueba una vez y se conserva una vez, sin importar cuántas devoluciones tenga. El SELECT 1 es una convención; EXISTS ignora lo que la subconsulta selecciona. La línea WHERE o.customer_id = c.customer_id la hace correlacionada: ata la consulta interna al cliente externo actual. Si la olvidas, EXISTS es verdadero para todos en cuanto existe cualquier devolución.

    IN

    WHERE c.customer_id IN (
      SELECT o.customer_id
      FROM orders AS o
      JOIN order_items AS i ON i.order_id = o.order_id
      JOIN returns AS r ON r.order_item_id = i.order_item_id
    )

    Mismo resultado, y a mucha gente le resulta más fácil de leer. Los duplicados en la lista no importan.

    NOT IN y la trampa del NULL

    Las formas negativas son donde difieren. x NOT IN (1, 2, NULL) significa x <> 1 AND x <> 2 AND x <> NULL. La última comparación es desconocida, así que toda la condición nunca es verdadera y no vuelve ninguna fila. Si la columna de la subconsulta puede contener aunque sea un NULL, NOT IN no devuelve nada:

    -- returns zero rows: web_sessions.customer_id is NULL for anonymous visitors
    SELECT customer_id FROM customers
    WHERE customer_id NOT IN (SELECT customer_id FROM web_sessions);

    NOT EXISTS no tiene ese problema, porque solo pregunta si existe una fila coincidente:

    WHERE NOT EXISTS (
      SELECT 1 FROM web_sessions AS s WHERE s.customer_id = c.customer_id
    )

    Regla práctica: IN y EXISTS son intercambiables para “tiene una coincidencia”. Para “no tiene coincidencia”, prefiere NOT EXISTS o el anti-join con LEFT JOIN del capítulo 5.

    Rendimiento

    Un consejo antiguo dice que EXISTS es más rápido que IN, o al revés. Los optimizadores modernos de Postgres, DuckDB, BigQuery y MySQL 8 convierten ambos en el mismo plan de semi-join la mayor parte del tiempo. Elige por corrección y legibilidad.

    Nota de entrevista

    “Encuentra los clientes que nunca visitaron el sitio” con un NOT IN que devuelve cero filas es una pregunta trampa favorita. Explicar el comportamiento del NULL, y arreglarlo con NOT EXISTS, es la respuesta que destaca.

    Errores comunes

    • Usar NOT IN sobre una columna que puede ser NULL. Un NULL en la lista hace que customer_id NOT IN (SELECT customer_id FROM web_sessions) devuelva cero filas. Usa NOT EXISTS.
    • Olvidar correlacionar la subconsulta. Sin WHERE o.customer_id = c.customer_id, EXISTS es verdadero para todos los clientes en cuanto existe cualquier devolución.
    • Hacer un join y añadir DISTINCT en su lugar. La salida parece correcta, pero en cuanto alguien añade un SUM a la consulta, cuenta cada cliente una vez por devolución.

    En SQL Server

    EXISTS, IN y NOT IN se comportan igual, trampa del NULL incluida: un NULL en una lista de NOT IN no devuelve filas. NOT EXISTS es también el anti-join recomendado en SQL Server.

    Elige qué puede ejecutarse en este sitio. Puedes cambiarlo cuando quieras desde "Configurar cookies", al pie de cada página.